From d1437a1002b818a43436ec2e87241980e1b94eb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:09:47 +0000 Subject: [PATCH 01/15] Rewrite plugin core in pure Ruby (pg driver) as groundwork for a ruby.wasm plugin Replace the google-protobuf C-extension based stub with a dependency-free protobuf codec and a Ruby port of the C# generator (pg driver). A golden test checks output against the C# plugin's examples/pg output. The wasm packaging script is included but unverified. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBk5e4S7G7aVhMoc9z --- ruby-wasm-poc/Gemfile | 13 +- ruby-wasm-poc/Gemfile.lock | 42 ---- ruby-wasm-poc/Makefile | 27 --- ruby-wasm-poc/README.md | 87 ++++---- ruby-wasm-poc/buf.gen.yaml | 4 - ruby-wasm-poc/exe/plugin.rb | 11 + ruby-wasm-poc/lib/gen/plugin/codegen_pb.rb | 30 --- ruby-wasm-poc/lib/plugin.rb | 41 ---- ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb | 40 ++++ ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb | 62 ++++++ ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb | 150 +++++++++++++ ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb | 22 ++ ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb | 69 ++++++ ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb | 200 ++++++++++++++++++ ruby-wasm-poc/scripts/build_wasm.sh | 27 +++ ruby-wasm-poc/scripts/generate_protobuf.sh | 25 --- ruby-wasm-poc/scripts/install_rbwasm.sh | 20 -- ruby-wasm-poc/scripts/sync_wasm_sha.sh | 4 - ruby-wasm-poc/test/fixture.rb | 74 +++++++ ruby-wasm-poc/test/golden_test.rb | 28 +++ 20 files changed, 739 insertions(+), 237 deletions(-) delete mode 100644 ruby-wasm-poc/Gemfile.lock delete mode 100644 ruby-wasm-poc/Makefile delete mode 100644 ruby-wasm-poc/buf.gen.yaml create mode 100644 ruby-wasm-poc/exe/plugin.rb delete mode 100644 ruby-wasm-poc/lib/gen/plugin/codegen_pb.rb delete mode 100644 ruby-wasm-poc/lib/plugin.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb create mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb create mode 100755 ruby-wasm-poc/scripts/build_wasm.sh delete mode 100755 ruby-wasm-poc/scripts/generate_protobuf.sh delete mode 100755 ruby-wasm-poc/scripts/install_rbwasm.sh delete mode 100755 ruby-wasm-poc/scripts/sync_wasm_sha.sh create mode 100644 ruby-wasm-poc/test/fixture.rb create mode 100644 ruby-wasm-poc/test/golden_test.rb diff --git a/ruby-wasm-poc/Gemfile b/ruby-wasm-poc/Gemfile index 29d26f4..b370ee9 100644 --- a/ruby-wasm-poc/Gemfile +++ b/ruby-wasm-poc/Gemfile @@ -1,6 +1,11 @@ source 'https://rubygems.org' -gem "ruby_wasm", "~> 2.6" -gem "ruby-next", "~> 1.0" -gem "google-protobuf", "~> 3.25" -gem "grpc", "~> 1.63" +# Build-time only: used to pack the plugin into ruby.wasm. +gem 'ruby_wasm', '~> 2.7' + +group :test do + # Test-time only (independent protobuf encoder/decoder used by test/fixture.rb). + # NOT used by the plugin itself - C extensions can't be loaded by ruby.wasm. + gem 'google-protobuf', '~> 4.0' + gem 'minitest' +end diff --git a/ruby-wasm-poc/Gemfile.lock b/ruby-wasm-poc/Gemfile.lock deleted file mode 100644 index 7a2640c..0000000 --- a/ruby-wasm-poc/Gemfile.lock +++ /dev/null @@ -1,42 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - ast (2.4.2) - diff-lcs (1.5.1) - google-protobuf (3.25.3-arm64-darwin) - googleapis-common-protos-types (1.14.0) - google-protobuf (~> 3.18) - grpc (1.63.0-arm64-darwin) - google-protobuf (~> 3.25) - googleapis-common-protos-types (~> 1.0) - paco (0.2.3) - parser (3.3.1.0) - ast (~> 2.4.1) - racc - racc (1.7.3) - require-hooks (0.2.2) - ruby-next (1.0.3) - paco (~> 0.2) - require-hooks (~> 0.2) - ruby-next-core (= 1.0.3) - ruby-next-parser (>= 3.2.2.0) - unparser (~> 0.6.0) - ruby-next-core (1.0.3) - ruby-next-parser (3.2.2.0) - parser (>= 3.0.3.1) - ruby_wasm (2.6.1-arm64-darwin) - unparser (0.6.13) - diff-lcs (~> 1.3) - parser (>= 3.3.0) - -PLATFORMS - arm64-darwin - -DEPENDENCIES - google-protobuf (~> 3.25) - grpc (~> 1.63) - ruby-next (~> 1.0) - ruby_wasm (~> 2.6) - -BUNDLED WITH - 2.5.10 diff --git a/ruby-wasm-poc/Makefile b/ruby-wasm-poc/Makefile deleted file mode 100644 index f0beaf0..0000000 --- a/ruby-wasm-poc/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -SHELL := /bin/bash -PWD := $(shell pwd) -RUBY_WASM := ${HOME}/bin/ruby-wasm - -protobuf-generate: - ./scripts/generate_protobuf.sh - -tebako-build: - tebako press \ - --root='.' \ - --entry=bin/plugin.rb \ - --output=dist/plugin.tebako \ - --Ruby=3.2.3 \ - --log-level=debug # TODO if removed - fails on segmentation fault - -wasm-build: - rbwasm build --remake --reconfigure --ruby-version 3.2 -o dist/plugin.wasm - echo "WASM filesize:" && du -sh dist/plugin.wasm - -sync-wasm-sha: protobuf-generate wasm-build - ./scripts/sync_wasm_sha.sh - -sqlc-generate-process: protobuf-generate tebako-build - sqlc -f sqlc.process.yaml generate - -sqlc-generate-wasm: sync-wasm-sha - SQLCCACHE=./; sqlc -f sqlc.wasm.yaml generate diff --git a/ruby-wasm-poc/README.md b/ruby-wasm-poc/README.md index cdc6cc5..3387a19 100644 --- a/ruby-wasm-poc/README.md +++ b/ruby-wasm-poc/README.md @@ -1,42 +1,49 @@ -# Ruby & WASM - POC - -## Background -Intended to generate code in Ruby, it will (hopefully) be used by Ruby developers. -I believe that the best way to keep an open-source project maintainable, is to have its users likely to maintain it.
-In the C# plugin I wrote for SQLC, I used C# Roslyn to generate my code. -When coming to implement a Ruby plugin, I wanted to take a similar approach. - -## The Blocker -In order to have a production SQLC plugin, we need to publish it as a standalone WASM file. -The approach I took was using the [rbwasm CLI](https://github.com/ruby/ruby.wasm) which was the only viable option I -found.
-After a lot of other errors that I did solve, I reached the error below and after some thorough investigation -I called quits on this angle - for now. - -```text -./build/checkouts/3.2/dln.c:275:5: error: use of undeclared identifier 'Dl_info' - 275 | Dl_info dli; - | ^ -./build/checkouts/3.2/dln.c:279:9: error: call to undeclared function 'dladdr'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration] - 279 | if (dladdr(ex, &dli)) { - | ^ -./build/checkouts/3.2/dln.c:279:21: error: use of undeclared identifier 'dli' - 279 | if (dladdr(ex, &dli)) { - | ^ -./ruby-wasm-poc/build/checkouts/3.2/dln.c:280:20: error: use of undeclared identifier 'dli' - 280 | *libname = dli.dli_fname; - | ^ -4 errors generated. +# sqlc-gen-ruby in Ruby (POC) + +Goal: write the sqlc plugin in Ruby instead of C#, and ship it as a `.wasm`. + +## What works (verified) + +The plugin logic is now **pure Ruby with zero native dependencies**: + +| File | Replaces | +|---|---| +| `lib/sqlc_gen_ruby/proto.rb` | `google-protobuf` (a C extension, which cannot run inside ruby.wasm). A ~150 line wire-format codec. | +| `lib/sqlc_gen_ruby/plugin_pb.rb` | The generated `GenerateRequest/Query/Column/...` classes (subset of `codegen.proto`). | +| `lib/sqlc_gen_ruby/generator.rb`, `drivers/pg.rb`, `options.rb`, `code.rb` | `CodeGenerator.cs`, `PgDriver.cs`, `MethodGen.cs`, `RubySyntax/*`, `PluginOptions/*`. | +| `exe/plugin.rb` | `PluginRunner.cs`: stdin = `GenerateRequest`, stdout = `GenerateResponse`. | + +`test/golden_test.rb` runs `exe/plugin.rb` as a subprocess over stdin/stdout (the way sqlc does) +with a request built by the official `google-protobuf` gem, and asserts the generated +`query_sql.rb` / `query_sql.rbs` equal what the C# plugin produced (`examples/pg`), ignoring CRs +and the trailing newline. + +```bash +cd ruby-wasm-poc && bundle install && bundle exec ruby test/golden_test.rb ``` -## Solution? -I reached the conclusion that, at this point, it's not worth the effort trying to get rbwasm to work for me, -So I decided not to let it block releasing a Ruby plugin, and I reused the code from my C# plugin to generate Ruby, -taking a vert Roslyn-inspired approach for it. - -### Going Forward -While the C# code works just fine, I think using Ruby to generate Ruby is a much cleaner approach then using C# to do -so, especially in the case of Ruby which is well known in its superb metaprogramming capabilities. -Of course contributions to the project will be welcome in general, but more specifically - if anyone is interested in -cooperating on the rbwasm approach, or some other approach that results in a Ruby code that is packaged in WASM file - -you're more then welcome to contact me. +Scope of the port: only the `pg` driver and the `:one` / `:many` / `:exec` commands. `mysql2`, +`sqlite3` and `:execlastid` still need porting (the structure in `drivers/pg.rb` is the template). + +## What is NOT verified: the `.wasm` + +This was developed in a sandbox where downloading and running the release binaries +(ruby.wasm, wasi-vfs, wasmtime, sqlc) was blocked, so **nothing was run inside WASM and +`sqlc generate` was never run against the plugin**. `scripts/build_wasm.sh` is the intended route: +use the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release (which sidesteps the old +`rbwasm build` `dln.c` / `Dl_info` failure) and `rbwasm pack` the stdlib and `lib/` + `bin/` into it. + +### Open problem: argv +sqlc runs a wasm plugin with no arguments, so a bare `ruby` would try to read its *script from stdin*, +which is where the protobuf request arrives. Something has to make the interpreter run +`/app/exe/plugin.rb`. Candidates, in order of preference to try: +1. Check whether `rbwasm pack` / wasi-vfs in the current version can bake in argv. +2. Wrap the module with a tiny shim that overrides `wasi_snapshot_preview1.args_get/args_sizes_get` + to return `["ruby", "/app/exe/plugin.rb"]` (e.g. via `wasm-merge`/`wasm-tools compose`). +3. Build a custom ruby.wasm whose `main` is fixed. + +### Next steps to finish the proof +1. `scripts/build_wasm.sh`, then `wasmtime run dist/plugin.wasm < request.bin` to check the wasm path. +2. Solve argv (above). +3. Point `sqlc.ci.yaml` at `dist/plugin.wasm` and run `sqlc generate`; diff against `examples/`. +4. Port the mysql2 / sqlite3 drivers. diff --git a/ruby-wasm-poc/buf.gen.yaml b/ruby-wasm-poc/buf.gen.yaml deleted file mode 100644 index 1ccca10..0000000 --- a/ruby-wasm-poc/buf.gen.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: v1 -plugins: - - plugin: buf.build/protocolbuffers/ruby - out: lib/gen \ No newline at end of file diff --git a/ruby-wasm-poc/exe/plugin.rb b/ruby-wasm-poc/exe/plugin.rb new file mode 100644 index 0000000..b6785b9 --- /dev/null +++ b/ruby-wasm-poc/exe/plugin.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# sqlc plugin entry point: GenerateRequest on stdin -> GenerateResponse on stdout. +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'sqlc_gen_ruby/generator' + +$stdin.binmode +$stdout.binmode +request = SqlcGenRuby::Plugin::GenerateRequest.decode($stdin.read) +response = SqlcGenRuby::Generator.new.generate(request) +$stdout.write(response.encode) diff --git a/ruby-wasm-poc/lib/gen/plugin/codegen_pb.rb b/ruby-wasm-poc/lib/gen/plugin/codegen_pb.rb deleted file mode 100644 index defa63b..0000000 --- a/ruby-wasm-poc/lib/gen/plugin/codegen_pb.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: plugin/codegen.proto - -require 'google/protobuf' - - -descriptor_data = "\n\x14plugin/codegen.proto\x12\x06plugin\"6\n\x04\x46ile\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n\x08\x63ontents\x18\x02 \x01(\x0cR\x08\x63ontents\"\xb7\x01\n\x08Settings\x12\x18\n\x07version\x18\x01 \x01(\tR\x07version\x12\x16\n\x06\x65ngine\x18\x02 \x01(\tR\x06\x65ngine\x12\x16\n\x06schema\x18\x03 \x03(\tR\x06schema\x12\x18\n\x07queries\x18\x04 \x03(\tR\x07queries\x12)\n\x07\x63odegen\x18\x0c \x01(\x0b\x32\x0f.plugin.CodegenR\x07\x63odegenJ\x04\x08\x05\x10\x06J\x04\x08\x08\x10\tJ\x04\x08\t\x10\nJ\x04\x08\n\x10\x0bJ\x04\x08\x0b\x10\x0c\"\x8b\x02\n\x07\x43odegen\x12\x10\n\x03out\x18\x01 \x01(\tR\x03out\x12\x16\n\x06plugin\x18\x02 \x01(\tR\x06plugin\x12\x18\n\x07options\x18\x03 \x01(\x0cR\x07options\x12\x10\n\x03\x65nv\x18\x04 \x03(\tR\x03\x65nv\x12\x31\n\x07process\x18\x05 \x01(\x0b\x32\x17.plugin.Codegen.ProcessR\x07process\x12(\n\x04wasm\x18\x06 \x01(\x0b\x32\x14.plugin.Codegen.WASMR\x04wasm\x1a\x1b\n\x07Process\x12\x10\n\x03\x63md\x18\x01 \x01(\tR\x03\x63md\x1a\x30\n\x04WASM\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x16\n\x06sha256\x18\x02 \x01(\tR\x06sha256\"\x88\x01\n\x07\x43\x61talog\x12\x18\n\x07\x63omment\x18\x01 \x01(\tR\x07\x63omment\x12%\n\x0e\x64\x65\x66\x61ult_schema\x18\x02 \x01(\tR\rdefaultSchema\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12(\n\x07schemas\x18\x04 \x03(\x0b\x32\x0e.plugin.SchemaR\x07schemas\"\xc1\x01\n\x06Schema\x12\x18\n\x07\x63omment\x18\x01 \x01(\tR\x07\x63omment\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12%\n\x06tables\x18\x03 \x03(\x0b\x32\r.plugin.TableR\x06tables\x12\"\n\x05\x65nums\x18\x04 \x03(\x0b\x32\x0c.plugin.EnumR\x05\x65nums\x12>\n\x0f\x63omposite_types\x18\x05 \x03(\x0b\x32\x15.plugin.CompositeTypeR\x0e\x63ompositeTypes\"=\n\rCompositeType\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07\x63omment\x18\x02 \x01(\tR\x07\x63omment\"H\n\x04\x45num\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n\x04vals\x18\x02 \x03(\tR\x04vals\x12\x18\n\x07\x63omment\x18\x03 \x01(\tR\x07\x63omment\"q\n\x05Table\x12$\n\x03rel\x18\x01 \x01(\x0b\x32\x12.plugin.IdentifierR\x03rel\x12(\n\x07\x63olumns\x18\x02 \x03(\x0b\x32\x0e.plugin.ColumnR\x07\x63olumns\x12\x18\n\x07\x63omment\x18\x03 \x01(\tR\x07\x63omment\"R\n\nIdentifier\x12\x18\n\x07\x63\x61talog\x18\x01 \x01(\tR\x07\x63\x61talog\x12\x16\n\x06schema\x18\x02 \x01(\tR\x06schema\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\"\x8e\x04\n\x06\x43olumn\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n\x08not_null\x18\x03 \x01(\x08R\x07notNull\x12\x19\n\x08is_array\x18\x04 \x01(\x08R\x07isArray\x12\x18\n\x07\x63omment\x18\x05 \x01(\tR\x07\x63omment\x12\x16\n\x06length\x18\x06 \x01(\x05R\x06length\x12$\n\x0eis_named_param\x18\x07 \x01(\x08R\x0cisNamedParam\x12 \n\x0cis_func_call\x18\x08 \x01(\x08R\nisFuncCall\x12\x14\n\x05scope\x18\t \x01(\tR\x05scope\x12(\n\x05table\x18\n \x01(\x0b\x32\x12.plugin.IdentifierR\x05table\x12\x1f\n\x0btable_alias\x18\x0b \x01(\tR\ntableAlias\x12&\n\x04type\x18\x0c \x01(\x0b\x32\x12.plugin.IdentifierR\x04type\x12\"\n\ris_sqlc_slice\x18\r \x01(\x08R\x0bisSqlcSlice\x12\x33\n\x0b\x65mbed_table\x18\x0e \x01(\x0b\x32\x12.plugin.IdentifierR\nembedTable\x12#\n\roriginal_name\x18\x0f \x01(\tR\x0coriginalName\x12\x1a\n\x08unsigned\x18\x10 \x01(\x08R\x08unsigned\x12\x1d\n\narray_dims\x18\x11 \x01(\x05R\tarrayDims\"\x94\x02\n\x05Query\x12\x12\n\x04text\x18\x01 \x01(\tR\x04text\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n\x03\x63md\x18\x03 \x01(\tR\x03\x63md\x12(\n\x07\x63olumns\x18\x04 \x03(\x0b\x32\x0e.plugin.ColumnR\x07\x63olumns\x12-\n\x06params\x18\x05 \x03(\x0b\x32\x11.plugin.ParameterR\nparameters\x12\x1a\n\x08\x63omments\x18\x06 \x03(\tR\x08\x63omments\x12\x1a\n\x08\x66ilename\x18\x07 \x01(\tR\x08\x66ilename\x12@\n\x11insert_into_table\x18\x08 \x01(\x0b\x32\x12.plugin.IdentifierR\x11insert_into_table\"K\n\tParameter\x12\x16\n\x06number\x18\x01 \x01(\x05R\x06number\x12&\n\x06\x63olumn\x18\x02 \x01(\x0b\x32\x0e.plugin.ColumnR\x06\x63olumn\"\x87\x02\n\x0fGenerateRequest\x12,\n\x08settings\x18\x01 \x01(\x0b\x32\x10.plugin.SettingsR\x08settings\x12)\n\x07\x63\x61talog\x18\x02 \x01(\x0b\x32\x0f.plugin.CatalogR\x07\x63\x61talog\x12\'\n\x07queries\x18\x03 \x03(\x0b\x32\r.plugin.QueryR\x07queries\x12\"\n\x0csqlc_version\x18\x04 \x01(\tR\x0csqlc_version\x12&\n\x0eplugin_options\x18\x05 \x01(\x0cR\x0eplugin_options\x12&\n\x0eglobal_options\x18\x06 \x01(\x0cR\x0eglobal_options\"6\n\x10GenerateResponse\x12\"\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x0c.plugin.FileR\x05\x66iles2O\n\x0e\x43odegenService\x12=\n\x08Generate\x12\x17.plugin.GenerateRequest\x1a\x18.plugin.GenerateResponseB*Z(github.com/sqlc-dev/sqlc/internal/pluginb\x06proto3" - -pool = Google::Protobuf::DescriptorPool.generated_pool -pool.add_serialized_file(descriptor_data) - -module Plugin - OutputFile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.File").msgclass - Settings = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Settings").msgclass - Codegen = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Codegen").msgclass - Codegen::Process = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Codegen.Process").msgclass - Codegen::WASM = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Codegen.WASM").msgclass - Catalog = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Catalog").msgclass - Schema = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Schema").msgclass - CompositeType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.CompositeType").msgclass - Enum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Enum").msgclass - Table = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Table").msgclass - Identifier = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Identifier").msgclass - Column = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Column").msgclass - Query = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Query").msgclass - Parameter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.Parameter").msgclass - GenerateRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.GenerateRequest").msgclass - GenerateResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("plugin.GenerateResponse").msgclass -end diff --git a/ruby-wasm-poc/lib/plugin.rb b/ruby-wasm-poc/lib/plugin.rb deleted file mode 100644 index 2572763..0000000 --- a/ruby-wasm-poc/lib/plugin.rb +++ /dev/null @@ -1,41 +0,0 @@ -require_relative 'gen/plugin/codegen_pb' -require 'json' - -module Plugin - class Runner - @options = {} - - def run - request_bytes = $stdin.read.bytes - generate_request = Plugin::GenerateRequest.decode(request_bytes) - generate_response = generate(generate_request) - $stdout.write(generate_response.encode) - end - - private - - def generate(generate_request) - @options = parse_options(generate_request['PluginOptions']) - files = generate_files_from_queries(generate_request) - generate_response = Plugin::GenerateResponse.new(:Files => files) - generate_response.encode - end - - def parse_options(plugin_options) - plugin_options_str = plugin_options - .bytes.pack('C*') - .force_encoding('UTF-8') - JSON.parse(plugin_options_str) - end - - def generate_files_from_queries(generate_request) - generate_request['Queries'] - .group_by { |query| query['Filename'] } - .map { |filename, queries| generate_file(filename, queries) } - end - - def generate_file(filename, queries) - Plugin.OutputFile.new(:Name => filename, :Contents => "".bytes) - end - end -end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb new file mode 100644 index 0000000..69fa4e8 --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module SqlcGenRuby + # Small helpers for building indented Ruby / RBS source text. + module Code + MAX_ELEMENTS_PER_LINE = 5 + + module_function + + def indent(text) + text.split("\n", -1).map { |l| "\t#{l}" }.join("\n") + end + + # Strips from every line as many leading characters as the first line has + # leading whitespace (keeps multi-line values aligned after being indented). + def dedent_to_first_line(text) + lines = text.split("\n", -1) + n = lines.first[/\A\s*/].length + lines.map { |l| l[n..] || '' }.join("\n") + end + + def comma_join(items) + return items.join(', ') if items.size < MAX_ELEMENTS_PER_LINE + + "\n#{indent(items.join(",\n"))}\n" + end + + def snake_case(str) + str.gsub(/([A-Z])/, '_\1').sub(/\A_+/, '').downcase + end + + def upcase_first(str) + str[0].upcase + str[1..] + end + + def block(header, body) + "#{header}\n#{indent(body)}\nend" + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb new file mode 100644 index 0000000..d9dbc48 --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require_relative '../code' + +module SqlcGenRuby + module Drivers + # Port of Drivers/PgDriver.cs. Each *_code / *_type pair mirrors the + # BuildCode / BuildType of the C# syntax tree nodes. + class Pg + include Code + + def name = 'Pg' + + def required_gems = %w[connection_pool pg set] + + COLUMN_TYPES = [ + ['bool', %w[bool boolean]], + ['Array[Integer]', %w[binary bit bytea blob longblob mediumblob tinyblob varbinary]], + ['String', %w[char date datetime longtext mediumtext text bpchar time timestamp tinytext varchar json]], + ['Integer', %w[int2 int4 int8 serial bigserial]], + ['Float', %w[numeric float4 float8 decimal]] + ].freeze + + def column_type(column) + db_type = column.type.name.downcase + COLUMN_TYPES.each { |ruby, dbs| return ruby if dbs.include?(db_type) } + raise "Unsupported column type: #{column.type.name}" + end + + def init_code + pool_body = [ + 'client = PG.connect(**pg_params)', + 'client.type_map_for_results = PG::BasicTypeMapForResults.new client', + 'client' + ].join("\n") + pool = "ConnectionPool.new(**connection_pool_params) {\n#{Code.indent(pool_body)}\n}" + body = ["@db = #{pool}", '@prepared_statements = Set[]'].join("\n") + Code.block('def initialize(connection_pool_params, pg_params)', body) + end + + def init_type + "@db: untyped\n@prepared_statements: Set[String]\n" \ + 'def initialize: (Hash[String, String], Hash[String, String]) -> void' + end + + def query_text_constant(query) + counter = 0 + text = query.text.gsub('?') { "$#{counter += 1}" } + Code.dedent_to_first_line("#{query.name}Sql = %q(#{text})") + end + + def prepare_stmt(func_name, const) + body = "client.prepare('#{func_name}', #{const})\n@prepared_statements.add('#{func_name}')" + Code.block("unless @prepared_statements.include?('#{func_name}')", body) + end + + def execute_stmt(func_name, has_params) + "client.exec_prepared('#{func_name}'#{has_params ? ', query_params' : ''})" + end + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb new file mode 100644 index 0000000..4201f6d --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require_relative 'code' +require_relative 'options' +require_relative 'plugin_pb' +require_relative 'drivers/pg' + +module SqlcGenRuby + # Ruby port of CodeGenerator/CodeGenerator.cs + Drivers/MethodGen.cs. + class Generator + include Code + + AUTO_GENERATED = '# auto-generated by sqlc - do not edit' + + def generate(request) + @options = Options.new(request.plugin_options) + @driver = instantiate_driver + files = request.queries.group_by { |q| class_name_for(q.filename) }.flat_map do |class_name, queries| + generate_files(queries, class_name) + end + files << gemfile if @options.generate_gemfile + Plugin::GenerateResponse.new(files: files) + end + + private + + def instantiate_driver + case @options.driver.downcase + when 'pg' then Drivers::Pg.new + else raise ArgumentError, "unknown or not-yet-ported driver: #{@options.driver}" + end + end + + # query.sql => QuerySql + def class_name_for(filename) + ext = File.extname(filename) + Code.upcase_first(File.basename(filename, ext)) + Code.upcase_first(ext[1..]) + end + + def generate_files(queries, class_name) + module_name = "#{@driver.name}Codegen" + members = queries.flat_map { |q| query_members(q) } + klass = class_members(queries) + base = Code.snake_case(class_name) + + code_body = (members.map(&:first) + [Code.block("class #{class_name}", klass.map(&:first).join("\n\n"))]) + gems = @driver.required_gems.map { |g| "require '#{g}'" }.join("\n") + code = "#{AUTO_GENERATED}\n#{gems}\n\n#{Code.block("module #{module_name}", code_body.join("\n\n"))}" + files = [Plugin::File.new(name: "#{base}.rb", contents: code)] + + if @options.generate_types + type_body = members.map(&:last).compact + [Code.block("class #{class_name}", klass.map(&:last).join("\n"))] + types = "#{AUTO_GENERATED}\n#{Code.block("module #{module_name}", type_body.join("\n\n"))}" + files << Plugin::File.new(name: "#{base}.rbs", contents: types) + end + files + end + + # [[code, rbs_or_nil], ...] for the SQL constant, row class, and args class. + def query_members(query) + members = [[@driver.query_text_constant(query), "#{query.name}Sql: String"]] + members << dataclass("#{Code.upcase_first(query.name)}Row", query.columns) unless query.columns.empty? + unless query.params.empty? + members << dataclass("#{Code.upcase_first(query.name)}Args", query.params.map(&:column)) + end + members + end + + def class_members(queries) + [[@driver.init_code, @driver.init_type]] + queries.map { |q| [method_code(q), method_type(q)] } + end + + def dataclass(name, columns) + pairs = columns.map { |c| [c.name, @driver.column_type(c)] } + @options.immutable_data_supported? ? data_define(name, pairs) : new_struct(name, pairs) + end + + def data_define(name, pairs) + attrs = Code.comma_join(pairs.map { |n, _| ":#{n}" }) + code = "class #{name} < Data.define(#{attrs})\nend" + readers = Code.indent(pairs.map { |n, t| "attr_reader #{n}: #{t}" }.join("\n")) + init_args = Code.indent(Code.comma_join(pairs.map { |n, t| "#{t} #{n}" })) + init = Code.indent("def initialize: (#{init_args}) -> void") + [code, "class #{name}\n#{readers}\n#{init}\nend"] + end + + def new_struct(name, pairs) + attrs = Code.comma_join(pairs.map(&:first)) + code = "class #{name} < Struct.new(#{attrs})\nend" + accessors = Code.indent(pairs.map { |n, t| "attr_accessor #{n}: #{t}" }.join("\n")) + init_args = Code.indent(Code.comma_join(pairs.map { |n, t| "#{t} #{n}" })) + init = Code.indent("def self.new: (#{init_args}) -> #{name}") + [code, "class #{name} < ::Struct\n#{accessors}\n#{init}\nend"] + end + + # --- methods (MethodGen) --- + + def method_code(query) + func = Code.snake_case(query.name) + const = "#{query.name}Sql" + args_var = Code.snake_case("#{query.name}Args") + has_params = !query.params.empty? + lines = [] + if has_params + list = Code.comma_join(query.params.map { |p| "#{args_var}.#{p.column.name}" }) + lines << "query_params = [#{list}]" + end + lines << @driver.prepare_stmt(func, const) + exec = @driver.execute_stmt(func, has_params) + + case query.cmd + when ':one' + lines << "result = #{exec}" + lines << 'row = result.first' + lines << 'return nil if row.nil?' + lines << "entity = #{new_row(query)}" + lines << 'return entity' + when ':many' + lines << "result = #{exec}" + lines << 'entities = []' + lines << Code.block('result.each do |row|', "entities << #{new_row(query)}") + lines << 'return entities' + when ':exec' + lines << exec + else + raise "unsupported query command: #{query.cmd}" + end + + params = has_params ? "(#{args_var})" : '' + Code.block("def #{func}#{params}", Code.block('@db.with do |client|', lines.join("\n"))) + end + + def new_row(query) + args = Code.comma_join(query.columns.map { |c| "row['#{c.name}']" }) + "#{query.name}Row.new(#{args})" + end + + def method_type(query) + func = Code.snake_case(query.name) + params = query.params.empty? ? '' : "(#{query.name}Args) " + ret = query.cmd == ':one' ? "#{query.name}Row?" : 'void' + "\ndef #{func}: #{params}-> #{ret}" + end + + def gemfile + gems = @driver.required_gems.map { |g| "gem '#{g}'" }.join("\n") + Plugin::File.new(name: 'Gemfile', contents: "source 'https://rubygems.org'\n\n#{gems}") + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb new file mode 100644 index 0000000..ea7366c --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'json' + +module SqlcGenRuby + class Options + attr_reader :driver, :ruby_version, :generate_gemfile, :generate_types + + def initialize(plugin_options_bytes) + text = plugin_options_bytes.to_s.dup.force_encoding(Encoding::UTF_8) + raw = text.strip.empty? ? {} : JSON.parse(text) + @driver = raw.fetch('driver') + @generate_gemfile = raw.fetch('generateGemfile', false) + @generate_types = raw.fetch('generateTypes', true) + @ruby_version = Gem::Version.new(raw.fetch('rubyVersion', '3.3')) + end + + def immutable_data_supported? + ruby_version >= Gem::Version.new('3.2') + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb new file mode 100644 index 0000000..52a223c --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require_relative 'proto' + +# Subset of sqlc's plugin/codegen.proto needed by the generator. +# Field numbers must match https://github.com/sqlc-dev/sqlc/blob/main/protos/plugin/codegen.proto +module SqlcGenRuby + module Plugin + P = Proto::Message + + class File < P + field :name, 1, :string + field :contents, 2, :bytes + end + + class Identifier < P + field :catalog, 1, :string + field :schema, 2, :string + field :name, 3, :string + end + + class Column < P + field :name, 1, :string + field :not_null, 3, :bool + field :is_array, 4, :bool + field :comment, 5, :string + field :length, 6, :int32 + field :is_named_param, 7, :bool + field :is_func_call, 8, :bool + field :scope, 9, :string + field :table, 10, :message, klass: Identifier + field :table_alias, 11, :string + field :type, 12, :message, klass: Identifier + field :is_sqlc_slice, 13, :bool + field :embed_table, 14, :message, klass: Identifier + field :original_name, 15, :string + field :unsigned, 16, :bool + field :array_dims, 17, :int32 + end + + class Parameter < P + field :number, 1, :int32 + field :column, 2, :message, klass: Column + end + + class Query < P + field :text, 1, :string + field :name, 2, :string + field :cmd, 3, :string + field :columns, 4, :message, repeated: true, klass: Column + field :params, 5, :message, repeated: true, klass: Parameter + field :comments, 6, :string, repeated: true + field :filename, 7, :string + field :insert_into_table, 8, :message, klass: Identifier + end + + class GenerateRequest < P + # settings(1) and catalog(2) are skipped: the generator does not need them. + field :queries, 3, :message, repeated: true, klass: Query + field :sqlc_version, 4, :string + field :plugin_options, 5, :bytes + field :global_options, 6, :bytes + end + + class GenerateResponse < P + field :files, 1, :message, repeated: true, klass: File + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb new file mode 100644 index 0000000..48530d9 --- /dev/null +++ b/ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +# A tiny, dependency-free protobuf (proto3) wire-format codec. +# +# The official `google-protobuf` gem is a C extension and cannot be loaded by a +# ruby.wasm interpreter, so the plugin carries its own minimal implementation. +# Only the features sqlc's plugin protocol needs are supported: varints, length +# delimited fields (string/bytes/messages), repeated fields. Unknown fields are +# skipped on decode. +module SqlcGenRuby + module Proto + VARINT = 0 + FIXED64 = 1 + LEN = 2 + FIXED32 = 5 + + Field = Struct.new(:name, :number, :type, :repeated, :klass) + + class Message + class << self + def fields + @fields ||= [] + end + + # scalar types: :string, :bytes, :bool, :int32, :int64, :uint32, :uint64 + # message types: :message with `klass:` + def field(name, number, type, repeated: false, klass: nil) + fields << Field.new(name, number, type, repeated, klass) + attr_writer name + if repeated + define_method(name) { instance_variable_get("@#{name}") || instance_variable_set("@#{name}", []) } + else + default = default_for(type) + define_method(name) do + v = instance_variable_get("@#{name}") + v.nil? ? (default.dup rescue default) : v + end + end + end + + def default_for(type) + case type + when :string then '' + when :bytes then ''.b + when :bool then false + when :message then nil + else 0 + end + end + + def decode(data) + msg = new + reader = Reader.new(data) + until reader.eof? + number, wire = reader.read_tag + f = fields.find { |fd| fd.number == number } + if f.nil? + reader.skip(wire) + next + end + value = reader.read_value(f, wire) + if f.repeated + msg.public_send(f.name) << value + else + msg.public_send("#{f.name}=", value) + end + end + msg + end + end + + def initialize(**attrs) + attrs.each { |k, v| public_send("#{k}=", v) } + end + + def encode + out = ''.b + self.class.fields.each do |f| + values = f.repeated ? public_send(f.name) : [instance_variable_get("@#{f.name}")] + values.each do |v| + next if v.nil? + next if !f.repeated && v == self.class.default_for(f.type) + + Writer.write_field(out, f, v) + end + end + out + end + end + + class Reader + def initialize(data) + @data = data.b + @pos = 0 + end + + def eof? + @pos >= @data.bytesize + end + + def read_varint + result = 0 + shift = 0 + loop do + raise 'truncated varint' if eof? + + byte = @data.getbyte(@pos) + @pos += 1 + result |= (byte & 0x7f) << shift + return result if byte < 0x80 + + shift += 7 + end + end + + def read_tag + tag = read_varint + [tag >> 3, tag & 7] + end + + def read_bytes + len = read_varint + raise 'truncated field' if @pos + len > @data.bytesize + + s = @data.byteslice(@pos, len) + @pos += len + s + end + + def skip(wire) + case wire + when VARINT then read_varint + when FIXED64 then @pos += 8 + when LEN then read_bytes + when FIXED32 then @pos += 4 + else raise "unsupported wire type #{wire}" + end + end + + def read_value(field, wire) + case field.type + when :string then expect(wire, LEN) { read_bytes.force_encoding(Encoding::UTF_8) } + when :bytes then expect(wire, LEN) { read_bytes } + when :message then expect(wire, LEN) { field.klass.decode(read_bytes) } + when :bool then expect(wire, VARINT) { read_varint != 0 } + when :int32 then expect(wire, VARINT) { to_signed(read_varint, 32) } + when :int64 then expect(wire, VARINT) { to_signed(read_varint, 64) } + when :uint32, :uint64 then expect(wire, VARINT) { read_varint } + else raise "unsupported type #{field.type}" + end + end + + private + + def expect(wire, wanted) + raise "wire type mismatch: got #{wire}, want #{wanted}" unless wire == wanted + + yield + end + + # int32 and int64 are both sign-extended to 64 bits on the wire. + def to_signed(v, _bits) + v >= (1 << 63) ? v - (1 << 64) : v + end + end + + module Writer + module_function + + def varint(out, n) + n &= 0xffffffffffffffff if n.negative? + loop do + byte = n & 0x7f + n >>= 7 + if n.zero? + out << byte.chr + break + end + out << (byte | 0x80).chr + end + end + + def write_field(out, f, v) + case f.type + when :string, :bytes, :message + payload = f.type == :message ? v.encode : v.to_s.b + varint(out, (f.number << 3) | LEN) + varint(out, payload.bytesize) + out << payload + when :bool + varint(out, (f.number << 3) | VARINT) + varint(out, v ? 1 : 0) + else + varint(out, (f.number << 3) | VARINT) + varint(out, v) + end + end + end + end +end diff --git a/ruby-wasm-poc/scripts/build_wasm.sh b/ruby-wasm-poc/scripts/build_wasm.sh new file mode 100755 index 0000000..ec23165 --- /dev/null +++ b/ruby-wasm-poc/scripts/build_wasm.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Packs the pure-Ruby plugin into a standalone WASI module (dist/plugin.wasm). +# +# STATUS: UNVERIFIED. Written without network access to the ruby.wasm release +# assets, so this has not been run end to end. See README.md ("Open problem: argv"). +set -euo pipefail +cd "$(dirname "$0")/.." + +RUBY_WASM_VERSION="${RUBY_WASM_VERSION:-2.7.1}" +RUBY_WASM_FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# 1. prebuilt CRuby compiled to wasm32-wasip1 (no need for `rbwasm build`, which is what broke before) +curl -fsSL -o "$WORK/ruby.tgz" \ + "https://github.com/ruby/ruby.wasm/releases/download/${RUBY_WASM_VERSION}/${RUBY_WASM_FLAVOR}.tar.gz" +tar -xzf "$WORK/ruby.tgz" -C "$WORK" + +# 2. embed the stdlib + our code in the wasm file itself via wasi-vfs (sqlc gives plugins no filesystem) +mkdir -p dist +bundle exec rbwasm pack "$WORK/${RUBY_WASM_FLAVOR}/usr/local/bin/ruby" \ + --dir "$WORK/${RUBY_WASM_FLAVOR}/usr/local/lib/ruby::/usr/local/lib/ruby" \ + --dir ./lib::/app/lib \ + --dir ./exe::/app/exe \ + -o dist/plugin.wasm + +du -h dist/plugin.wasm diff --git a/ruby-wasm-poc/scripts/generate_protobuf.sh b/ruby-wasm-poc/scripts/generate_protobuf.sh deleted file mode 100755 index 8574fa1..0000000 --- a/ruby-wasm-poc/scripts/generate_protobuf.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -replace_in_file () { - text_to_replace=$1 - replace_with=$2 - filename=$3 - - if [[ "$(uname -s)" == "Linux" ]]; then - sed -i "s!${text_to_replace}!${replace_with}!g" "${filename}" - else - sed -i '' "s!${text_to_replace}!${replace_with}!g" "${filename}" - fi -} - -if wget -q --spider http://google.com; then - buf generate --template buf.gen.yaml buf.build/sqlc/sqlc --path plugin/ - - # File is a builtin class library in Ruby, and it's a bad practice to name a class as a builtin class. - # Since we have limited direct control on the generated protobuf code, we fix it post-generate. - replace_in_file 'File[[:blank:]]' 'OutputFile ' lib/gen/plugin/codegen_pb.rb -else - echo "No internet connection - using pre-existing protobuf files.." -fi \ No newline at end of file diff --git a/ruby-wasm-poc/scripts/install_rbwasm.sh b/ruby-wasm-poc/scripts/install_rbwasm.sh deleted file mode 100755 index aa9ab04..0000000 --- a/ruby-wasm-poc/scripts/install_rbwasm.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -# uninstall -gem uninstall ruby_wasm --all --executables -asdf uninstall ruby 3.2.2 || echo "ruby not installed" - -# install -gem install ruby_wasm -brew install libyaml -gem install psych -- --with-libyaml-dir="$(brew --prefix libyaml)" -RUBY_CONFIGURE_OPTS="--with-libyaml-dir=$(brew --prefix libyaml)" -export RUBY_CONFIGURE_OPTS -asdf install ruby 3.2.2 && asdf global ruby 3.2.2 - -cd /tmp && curl -LO --silent \ - https://github.com/ruby/ruby.wasm/releases/download/2.6.1/ruby-3.2-wasm32-unknown-wasip1-full.tar.gz -tar xvf ruby-3.2-wasm32-unknown-wasip1-full.tar.gz -mv /tmp/ruby-3.2-wasm32-unknown-wasip1-full/usr/local/bin/ruby ~/bin/ruby.wasm \ No newline at end of file diff --git a/ruby-wasm-poc/scripts/sync_wasm_sha.sh b/ruby-wasm-poc/scripts/sync_wasm_sha.sh deleted file mode 100755 index 940b24b..0000000 --- a/ruby-wasm-poc/scripts/sync_wasm_sha.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -PLUGIN_SHA=$(shasum -a 256 dist/plugin.wasm | awk '{ print $1 }') -yq -i ".plugins[0].wasm.sha256 = \"${PLUGIN_SHA}\"" sqlc.wasm.yaml \ No newline at end of file diff --git a/ruby-wasm-poc/test/fixture.rb b/ruby-wasm-poc/test/fixture.rb new file mode 100644 index 0000000..a77bd5e --- /dev/null +++ b/ruby-wasm-poc/test/fixture.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Builds a GenerateRequest equivalent to what sqlc would send for +# examples/authors/postgresql (hand-built, since sqlc isn't runnable here). +# Uses the official google-protobuf gem as an independent encoder so the +# plugin's pure-Ruby decoder is verified against a real protobuf implementation. +require 'google/protobuf' + +module Fixture + POOL = Google::Protobuf::DescriptorPool.generated_pool + # Minimal schema mirroring sqlc's codegen.proto (same field numbers). + require 'google/protobuf/descriptor_pb' + fdp = Google::Protobuf::FileDescriptorProto.new(name: 'fx.proto', package: 'plugin', syntax: 'proto3') + add = lambda do |name, fields| + m = Google::Protobuf::DescriptorProto.new(name: name) + fields.each do |fname, num, type, label, tname| + m.field << Google::Protobuf::FieldDescriptorProto.new( + name: fname, number: num, type: type, label: label || :LABEL_OPTIONAL, type_name: tname + ) + end + fdp.message_type << m + end + rep = :LABEL_REPEATED + add.call('Identifier', [['catalog', 1, :TYPE_STRING], ['schema', 2, :TYPE_STRING], ['name', 3, :TYPE_STRING]]) + add.call('Column', [['name', 1, :TYPE_STRING], ['not_null', 3, :TYPE_BOOL], ['length', 6, :TYPE_INT32], + ['type', 12, :TYPE_MESSAGE, nil, '.plugin.Identifier']]) + add.call('Parameter', [['number', 1, :TYPE_INT32], ['column', 2, :TYPE_MESSAGE, nil, '.plugin.Column']]) + add.call('Query', [['text', 1, :TYPE_STRING], ['name', 2, :TYPE_STRING], ['cmd', 3, :TYPE_STRING], + ['columns', 4, :TYPE_MESSAGE, rep, '.plugin.Column'], + ['params', 5, :TYPE_MESSAGE, rep, '.plugin.Parameter'], + ['filename', 7, :TYPE_STRING]]) + add.call('Settings', [['version', 1, :TYPE_STRING], ['engine', 2, :TYPE_STRING]]) + add.call('GenerateRequest', [['settings', 1, :TYPE_MESSAGE, nil, '.plugin.Settings'], + ['queries', 3, :TYPE_MESSAGE, rep, '.plugin.Query'], + ['sqlc_version', 4, :TYPE_STRING], ['plugin_options', 5, :TYPE_BYTES]]) + add.call('OutFile', [['name', 1, :TYPE_STRING], ['contents', 2, :TYPE_BYTES]]) + add.call('GenerateResponse', [['files', 1, :TYPE_MESSAGE, rep, '.plugin.OutFile']]) + POOL.add_serialized_file(Google::Protobuf::FileDescriptorProto.encode(fdp)) + # Materialize every message class up front (repeated fields resolve element types lazily). + fdp.message_type.each { |m| POOL.lookup("plugin.#{m.name}").msgclass } + + def self.k(name) = POOL.lookup("plugin.#{name}").msgclass + + def self.col(name, type) + k('Column').new(name: name, not_null: true, type: k('Identifier').new(name: type)) + end + + AUTHORS = [col('id', 'bigserial'), col('name', 'text'), col('bio', 'text')].freeze + TYPES = %w[c_bit:bit c_smallint:int2 c_boolean:bool c_integer:int4 c_bigint:int8 c_serial:serial + c_decimal:decimal c_numeric:numeric c_real:float4 c_double_precision:float8 c_date:date + c_time:time c_timestamp:timestamp c_char:bpchar c_varchar:varchar c_bytea:bytea c_text:text + c_json:json].map { |s| col(*s.split(':')) }.freeze + + def self.param(n, c) = k('Parameter').new(number: n, column: c) + + def self.query(name, cmd, text, cols, params) + k('Query').new(name: name, cmd: cmd, text: text, columns: cols, params: params, filename: 'query.sql') + end + + def self.request + qs = [ + query('GetAuthor', ':one', "SELECT id, name, bio FROM authors\nWHERE id = $1 LIMIT 1", AUTHORS, [param(1, AUTHORS[0])]), + query('ListAuthors', ':many', "SELECT id, name, bio FROM authors\nORDER BY name", AUTHORS, []), + query('CreateAuthor', ':one', + "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio", AUTHORS, + [param(1, AUTHORS[1]), param(2, AUTHORS[2])]), + query('DeleteAuthor', ':exec', "DELETE FROM authors\nWHERE id = $1", [], [param(1, AUTHORS[0])]), + query('Test', ':one', + "SELECT #{TYPES.map(&:name).join(', ')} FROM node_postgres_types\nLIMIT 1", TYPES, []) + ] + opts = '{"driver":"pg","rubyVersion":"3.3","generateTypes":true,"generateGemfile":false}' + k('GenerateRequest').new(queries: qs, plugin_options: opts, sqlc_version: 'v1.27.0') + end +end diff --git a/ruby-wasm-poc/test/golden_test.rb b/ruby-wasm-poc/test/golden_test.rb new file mode 100644 index 0000000..ace62ea --- /dev/null +++ b/ruby-wasm-poc/test/golden_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Runs the real plugin entry point (exe/plugin.rb) as a subprocess over stdin/stdout +# exactly the way sqlc runs it, then diffs the output with the C# plugin's output +# checked in under examples/pg. +require 'open3' +require 'minitest/autorun' +require_relative 'fixture' + +class GoldenTest < Minitest::Test + ROOT = File.expand_path('..', __dir__) + EXAMPLES = File.expand_path('../../examples/pg', __dir__) + + def setup + out, err, st = Open3.capture3('ruby', File.join(ROOT, 'exe/plugin.rb'), + stdin_data: Fixture.k('GenerateRequest').encode(Fixture.request), binmode: true) + assert st.success?, err + @files = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } + end + + def normalize(s) = s.delete("\r").strip + + %w[query_sql.rb query_sql.rbs].each do |name| + define_method("test_#{name}_matches_csharp_output") do + assert_equal normalize(File.read(File.join(EXAMPLES, name))), normalize(@files.fetch(name)) + end + end +end From 3588d75df5701a06605c9035f672733855a12dc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:09:53 +0000 Subject: [PATCH 02/15] docs: fix exe path Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBk5e4S7G7aVhMoc9z --- ruby-wasm-poc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby-wasm-poc/README.md b/ruby-wasm-poc/README.md index 3387a19..9c5a6eb 100644 --- a/ruby-wasm-poc/README.md +++ b/ruby-wasm-poc/README.md @@ -31,7 +31,7 @@ This was developed in a sandbox where downloading and running the release binari (ruby.wasm, wasi-vfs, wasmtime, sqlc) was blocked, so **nothing was run inside WASM and `sqlc generate` was never run against the plugin**. `scripts/build_wasm.sh` is the intended route: use the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release (which sidesteps the old -`rbwasm build` `dln.c` / `Dl_info` failure) and `rbwasm pack` the stdlib and `lib/` + `bin/` into it. +`rbwasm build` `dln.c` / `Dl_info` failure) and `rbwasm pack` the stdlib and `lib/` + `exe/` into it. ### Open problem: argv sqlc runs a wasm plugin with no arguments, so a bare `ruby` would try to read its *script from stdin*, From dfef70580375205708a5f971e5871edcd921bb53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:14:09 +0000 Subject: [PATCH 03/15] Build and run the Ruby plugin as a WASM sqlc plugin Pack prebuilt ruby.wasm with the stdlib and plugin via rbwasm pack, and embed a boot script at /plugin.CodegenService/Generate (the path sqlc passes as argv). Verified with sqlc v1.27.0: generated pg output matches the C# plugin. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBk5e4S7G7aVhMoc9z --- ruby-wasm-poc/README.md | 48 ++++++++++++++++------------- ruby-wasm-poc/scripts/build_wasm.sh | 34 ++++++++++++-------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/ruby-wasm-poc/README.md b/ruby-wasm-poc/README.md index 9c5a6eb..91f138d 100644 --- a/ruby-wasm-poc/README.md +++ b/ruby-wasm-poc/README.md @@ -25,25 +25,29 @@ cd ruby-wasm-poc && bundle install && bundle exec ruby test/golden_test.rb Scope of the port: only the `pg` driver and the `:one` / `:many` / `:exec` commands. `mysql2`, `sqlite3` and `:execlastid` still need porting (the structure in `drivers/pg.rb` is the template). -## What is NOT verified: the `.wasm` - -This was developed in a sandbox where downloading and running the release binaries -(ruby.wasm, wasi-vfs, wasmtime, sqlc) was blocked, so **nothing was run inside WASM and -`sqlc generate` was never run against the plugin**. `scripts/build_wasm.sh` is the intended route: -use the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release (which sidesteps the old -`rbwasm build` `dln.c` / `Dl_info` failure) and `rbwasm pack` the stdlib and `lib/` + `exe/` into it. - -### Open problem: argv -sqlc runs a wasm plugin with no arguments, so a bare `ruby` would try to read its *script from stdin*, -which is where the protobuf request arrives. Something has to make the interpreter run -`/app/exe/plugin.rb`. Candidates, in order of preference to try: -1. Check whether `rbwasm pack` / wasi-vfs in the current version can bake in argv. -2. Wrap the module with a tiny shim that overrides `wasi_snapshot_preview1.args_get/args_sizes_get` - to return `["ruby", "/app/exe/plugin.rb"]` (e.g. via `wasm-merge`/`wasm-tools compose`). -3. Build a custom ruby.wasm whose `main` is fixed. - -### Next steps to finish the proof -1. `scripts/build_wasm.sh`, then `wasmtime run dist/plugin.wasm < request.bin` to check the wasm path. -2. Solve argv (above). -3. Point `sqlc.ci.yaml` at `dist/plugin.wasm` and run `sqlc generate`; diff against `examples/`. -4. Port the mysql2 / sqlite3 drivers. +## The WASM plugin (verified end to end) + +`scripts/build_wasm.sh` produces `dist/plugin.wasm` (~37 MB, one self-contained file): + +1. downloads the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release (this sidesteps the old + `rbwasm build` `dln.c` / `Dl_info` failure entirely); +2. stages a slim filesystem (core stdlib minus bundler/rdoc/irb/etc. + `lib/` + `exe/`); +3. embeds it into the interpreter with `rbwasm pack` (wasi-vfs), since sqlc gives plugins no filesystem. + +**The argv problem is solved by a trick:** sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, +so the interpreter treats that path as its script. The build embeds a file at exactly that path containing +`load '/app/exe/plugin.rb'`. + +Verified with sqlc v1.27.0 (`wasm: url: file://...dist/plugin.wasm`) on `examples/authors/postgresql`: +`sqlc generate` takes ~2s and the generated `query_sql.rb` / `.rbs` are identical to the C# plugin's +(`examples/pg`), ignoring CRs. Also checked under wasmtime v27: WASM output is byte-identical to native Ruby. + +```bash +gem install ruby_wasm && ruby-wasm-poc/scripts/build_wasm.sh # set RBWASM="ruby /path/to/rbwasm" if rbwasm isn't on PATH +``` + +## Remaining work +- Port the `mysql2` and `sqlite3` drivers and `:execlastid` (use `drivers/pg.rb` as the template). +- Port the remaining C# options/edge cases (e.g. `generateGemfile` path is written but untested). +- Shrink the wasm (37 MB, mostly the interpreter) and wire it into CI (`sqlc.ci.yaml`). +- Decide on removing the C# projects once the Ruby port has parity. diff --git a/ruby-wasm-poc/scripts/build_wasm.sh b/ruby-wasm-poc/scripts/build_wasm.sh index ec23165..4511463 100755 --- a/ruby-wasm-poc/scripts/build_wasm.sh +++ b/ruby-wasm-poc/scripts/build_wasm.sh @@ -1,27 +1,37 @@ #!/usr/bin/env bash # Packs the pure-Ruby plugin into a standalone WASI module (dist/plugin.wasm). -# -# STATUS: UNVERIFIED. Written without network access to the ruby.wasm release -# assets, so this has not been run end to end. See README.md ("Open problem: argv"). +# Needs: ruby + `gem install ruby_wasm` (for `rbwasm pack`), curl, tar. set -euo pipefail cd "$(dirname "$0")/.." +PLUGIN_DIR="$(pwd)" RUBY_WASM_VERSION="${RUBY_WASM_VERSION:-2.7.1}" -RUBY_WASM_FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" +FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" +RBWASM="${RBWASM:-rbwasm}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT -# 1. prebuilt CRuby compiled to wasm32-wasip1 (no need for `rbwasm build`, which is what broke before) +# 1. prebuilt CRuby compiled to wasm32-wasip1 (avoids `rbwasm build`, which failed on dln.c/Dl_info) curl -fsSL -o "$WORK/ruby.tgz" \ - "https://github.com/ruby/ruby.wasm/releases/download/${RUBY_WASM_VERSION}/${RUBY_WASM_FLAVOR}.tar.gz" + "https://github.com/ruby/ruby.wasm/releases/download/${RUBY_WASM_VERSION}/${FLAVOR}.tar.gz" tar -xzf "$WORK/ruby.tgz" -C "$WORK" +SRC="$WORK/$FLAVOR/usr/local" -# 2. embed the stdlib + our code in the wasm file itself via wasi-vfs (sqlc gives plugins no filesystem) +# 2. stage a slim filesystem to embed: core stdlib only + the plugin. sqlc gives plugins no filesystem. +STAGE="$WORK/stage" +mkdir -p "$STAGE/usr/local/lib/ruby" "$STAGE/app" +cp -r "$SRC/lib/ruby/3.3.0" "$STAGE/usr/local/lib/ruby/3.3.0" +(cd "$STAGE/usr/local/lib/ruby/3.3.0" && rm -rf bundler rdoc irb reline net ruby_vm rbs typeprof debug) +mkdir -p "$STAGE/usr/local/lib/ruby/site_ruby" "$STAGE/usr/local/lib/ruby/vendor_ruby" "$STAGE/usr/local/lib/ruby/gems" +cp -r lib exe "$STAGE/app/" +# sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, so the interpreter treats that +# path as its script. Embed a file there that boots the real entry point. +mkdir -p "$STAGE/plugin.CodegenService" +echo "load '/app/exe/plugin.rb'" > "$STAGE/plugin.CodegenService/Generate" + +# 3. embed it into the interpreter with wasi-vfs mkdir -p dist -bundle exec rbwasm pack "$WORK/${RUBY_WASM_FLAVOR}/usr/local/bin/ruby" \ - --dir "$WORK/${RUBY_WASM_FLAVOR}/usr/local/lib/ruby::/usr/local/lib/ruby" \ - --dir ./lib::/app/lib \ - --dir ./exe::/app/exe \ +$RBWASM pack "$SRC/bin/ruby" \ + --dir "$STAGE/usr::/usr" --dir "$STAGE/app::/app" --dir "$STAGE/plugin.CodegenService::/plugin.CodegenService" \ -o dist/plugin.wasm - du -h dist/plugin.wasm From 97372ffb19f7f851a7fc9567bbb51c788c80ecdd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:30:50 +0000 Subject: [PATCH 04/15] Port mysql2/sqlite3 drivers, :execlastid and options to Ruby; move plugin to repo root - drivers/base.rb: shared Driver base (column mapping incl. prefix-based matching, pooling / row-access flavour), pg/mysql2/sqlite3 on top of it - method_gen.rb: port of MethodGen.cs (:one/:many/:exec/:execlastid) - ruby_version.rb + options.rb: port of RubyVersion/Options incl. min/max version errors and Struct (Ruby < 3.2) vs Data selection - move ruby-wasm-poc/{lib,exe,test} to the repo root, build script to scripts/wasm/build.sh - tests use real GenerateRequest bytes captured from sqlc v1.27.0 (test/fixtures, scripts/capture_request.rb) plus snapshots of the C# plugin's output for option variants (test/golden) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- Gemfile | 15 +- Gemfile.lock | 14 +- ruby-wasm-poc/README.md => docs/Design.md | 0 exe/plugin.rb | 19 ++ .../lib => lib}/sqlc_gen_ruby/code.rb | 2 + lib/sqlc_gen_ruby/drivers/base.rb | 78 +++++ lib/sqlc_gen_ruby/drivers/mysql2.rb | 43 +++ .../lib => lib}/sqlc_gen_ruby/drivers/pg.rb | 46 ++- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 40 +++ .../lib => lib}/sqlc_gen_ruby/generator.rb | 68 +--- lib/sqlc_gen_ruby/method_gen.rb | 92 ++++++ lib/sqlc_gen_ruby/options.rb | 44 +++ .../lib => lib}/sqlc_gen_ruby/plugin_pb.rb | 0 .../lib => lib}/sqlc_gen_ruby/proto.rb | 2 + lib/sqlc_gen_ruby/ruby_version.rb | 88 ++++++ ruby-wasm-poc/Gemfile | 11 - ruby-wasm-poc/exe/plugin.rb | 11 - ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb | 22 -- ruby-wasm-poc/test/golden_test.rb | 28 -- scripts/capture_request.rb | 28 ++ .../build_wasm.sh => scripts/wasm/build.sh | 4 +- test/codec_test.rb | 34 ++ test/extended_golden_test.rb | 24 ++ test/fixtures/mysql2.bin | 100 ++++++ test/fixtures/mysql2_extended.bin | 133 ++++++++ test/fixtures/pg.bin | 66 ++++ test/fixtures/pg_extended.bin | 98 ++++++ test/fixtures/sqlite3.bin | 26 ++ test/fixtures/sqlite3_extended.bin | 59 ++++ .../mysql2_extended/default/my_queries_sql.rb | 27 ++ .../default/my_queries_sql.rbs | 16 + .../mysql2_extended/default/options.json | 2 + .../mysql2_extended/default/other_sql.rb | 56 ++++ .../mysql2_extended/default/other_sql.rbs | 35 +++ .../mysql2_extended/default/query_sql.rb | 294 ++++++++++++++++++ .../mysql2_extended/default/query_sql.rbs | 217 +++++++++++++ .../mysql2_extended/ruby31_gemfile/Gemfile | 4 + .../ruby31_gemfile/my_queries_sql.rb | 27 ++ .../ruby31_gemfile/my_queries_sql.rbs | 16 + .../ruby31_gemfile/options.json | 4 + .../ruby31_gemfile/other_sql.rb | 56 ++++ .../ruby31_gemfile/other_sql.rbs | 35 +++ .../ruby31_gemfile/query_sql.rb | 294 ++++++++++++++++++ .../ruby31_gemfile/query_sql.rbs | 217 +++++++++++++ .../ruby32_notypes/my_queries_sql.rb | 27 ++ .../ruby32_notypes/options.json | 4 + .../ruby32_notypes/other_sql.rb | 56 ++++ .../ruby32_notypes/query_sql.rb | 294 ++++++++++++++++++ .../pg_extended/default/my_queries_sql.rb | 34 ++ .../pg_extended/default/my_queries_sql.rbs | 17 + test/golden/pg_extended/default/options.json | 2 + test/golden/pg_extended/default/other_sql.rb | 66 ++++ test/golden/pg_extended/default/other_sql.rbs | 36 +++ test/golden/pg_extended/default/query_sql.rb | 259 +++++++++++++++ test/golden/pg_extended/default/query_sql.rbs | 167 ++++++++++ .../golden/pg_extended/ruby31_gemfile/Gemfile | 5 + .../ruby31_gemfile/my_queries_sql.rb | 34 ++ .../ruby31_gemfile/my_queries_sql.rbs | 17 + .../pg_extended/ruby31_gemfile/options.json | 4 + .../pg_extended/ruby31_gemfile/other_sql.rb | 66 ++++ .../pg_extended/ruby31_gemfile/other_sql.rbs | 36 +++ .../pg_extended/ruby31_gemfile/query_sql.rb | 259 +++++++++++++++ .../pg_extended/ruby31_gemfile/query_sql.rbs | 167 ++++++++++ .../ruby32_notypes/my_queries_sql.rb | 34 ++ .../pg_extended/ruby32_notypes/options.json | 4 + .../pg_extended/ruby32_notypes/other_sql.rb | 66 ++++ .../pg_extended/ruby32_notypes/query_sql.rb | 259 +++++++++++++++ .../default/my_queries_sql.rb | 24 ++ .../default/my_queries_sql.rbs | 16 + .../sqlite3_extended/default/options.json | 2 + .../sqlite3_extended/default/other_sql.rb | 52 ++++ .../sqlite3_extended/default/other_sql.rbs | 35 +++ .../sqlite3_extended/default/query_sql.rb | 159 ++++++++++ .../sqlite3_extended/default/query_sql.rbs | 114 +++++++ .../sqlite3_extended/ruby31_gemfile/Gemfile | 4 + .../ruby31_gemfile/my_queries_sql.rb | 24 ++ .../ruby31_gemfile/my_queries_sql.rbs | 16 + .../ruby31_gemfile/options.json | 4 + .../ruby31_gemfile/other_sql.rb | 52 ++++ .../ruby31_gemfile/other_sql.rbs | 35 +++ .../ruby31_gemfile/query_sql.rb | 159 ++++++++++ .../ruby31_gemfile/query_sql.rbs | 114 +++++++ .../ruby32_notypes/my_queries_sql.rb | 24 ++ .../ruby32_notypes/options.json | 4 + .../ruby32_notypes/other_sql.rb | 52 ++++ .../ruby32_notypes/query_sql.rb | 159 ++++++++++ test/golden_test.rb | 22 ++ .../fixture.rb => test/official_protobuf.rb | 6 +- test/options_test.rb | 54 ++++ test/test_helper.rb | 41 +++ 90 files changed, 5422 insertions(+), 158 deletions(-) rename ruby-wasm-poc/README.md => docs/Design.md (100%) create mode 100755 exe/plugin.rb rename {ruby-wasm-poc/lib => lib}/sqlc_gen_ruby/code.rb (93%) create mode 100644 lib/sqlc_gen_ruby/drivers/base.rb create mode 100644 lib/sqlc_gen_ruby/drivers/mysql2.rb rename {ruby-wasm-poc/lib => lib}/sqlc_gen_ruby/drivers/pg.rb (53%) create mode 100644 lib/sqlc_gen_ruby/drivers/sqlite3.rb rename {ruby-wasm-poc/lib => lib}/sqlc_gen_ruby/generator.rb (66%) create mode 100644 lib/sqlc_gen_ruby/method_gen.rb create mode 100644 lib/sqlc_gen_ruby/options.rb rename {ruby-wasm-poc/lib => lib}/sqlc_gen_ruby/plugin_pb.rb (100%) rename {ruby-wasm-poc/lib => lib}/sqlc_gen_ruby/proto.rb (99%) create mode 100644 lib/sqlc_gen_ruby/ruby_version.rb delete mode 100644 ruby-wasm-poc/Gemfile delete mode 100644 ruby-wasm-poc/exe/plugin.rb delete mode 100644 ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb delete mode 100644 ruby-wasm-poc/test/golden_test.rb create mode 100755 scripts/capture_request.rb rename ruby-wasm-poc/scripts/build_wasm.sh => scripts/wasm/build.sh (97%) create mode 100644 test/codec_test.rb create mode 100644 test/extended_golden_test.rb create mode 100644 test/fixtures/mysql2.bin create mode 100644 test/fixtures/mysql2_extended.bin create mode 100644 test/fixtures/pg.bin create mode 100644 test/fixtures/pg_extended.bin create mode 100644 test/fixtures/sqlite3.bin create mode 100644 test/fixtures/sqlite3_extended.bin create mode 100644 test/golden/mysql2_extended/default/my_queries_sql.rb create mode 100644 test/golden/mysql2_extended/default/my_queries_sql.rbs create mode 100644 test/golden/mysql2_extended/default/options.json create mode 100644 test/golden/mysql2_extended/default/other_sql.rb create mode 100644 test/golden/mysql2_extended/default/other_sql.rbs create mode 100644 test/golden/mysql2_extended/default/query_sql.rb create mode 100644 test/golden/mysql2_extended/default/query_sql.rbs create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/Gemfile create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/options.json create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb create mode 100644 test/golden/mysql2_extended/ruby32_notypes/options.json create mode 100644 test/golden/mysql2_extended/ruby32_notypes/other_sql.rb create mode 100644 test/golden/mysql2_extended/ruby32_notypes/query_sql.rb create mode 100644 test/golden/pg_extended/default/my_queries_sql.rb create mode 100644 test/golden/pg_extended/default/my_queries_sql.rbs create mode 100644 test/golden/pg_extended/default/options.json create mode 100644 test/golden/pg_extended/default/other_sql.rb create mode 100644 test/golden/pg_extended/default/other_sql.rbs create mode 100644 test/golden/pg_extended/default/query_sql.rb create mode 100644 test/golden/pg_extended/default/query_sql.rbs create mode 100644 test/golden/pg_extended/ruby31_gemfile/Gemfile create mode 100644 test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb create mode 100644 test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs create mode 100644 test/golden/pg_extended/ruby31_gemfile/options.json create mode 100644 test/golden/pg_extended/ruby31_gemfile/other_sql.rb create mode 100644 test/golden/pg_extended/ruby31_gemfile/other_sql.rbs create mode 100644 test/golden/pg_extended/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/pg_extended/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb create mode 100644 test/golden/pg_extended/ruby32_notypes/options.json create mode 100644 test/golden/pg_extended/ruby32_notypes/other_sql.rb create mode 100644 test/golden/pg_extended/ruby32_notypes/query_sql.rb create mode 100644 test/golden/sqlite3_extended/default/my_queries_sql.rb create mode 100644 test/golden/sqlite3_extended/default/my_queries_sql.rbs create mode 100644 test/golden/sqlite3_extended/default/options.json create mode 100644 test/golden/sqlite3_extended/default/other_sql.rb create mode 100644 test/golden/sqlite3_extended/default/other_sql.rbs create mode 100644 test/golden/sqlite3_extended/default/query_sql.rb create mode 100644 test/golden/sqlite3_extended/default/query_sql.rbs create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/Gemfile create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/options.json create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb create mode 100644 test/golden/sqlite3_extended/ruby32_notypes/options.json create mode 100644 test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb create mode 100644 test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb create mode 100644 test/golden_test.rb rename ruby-wasm-poc/test/fixture.rb => test/official_protobuf.rb (93%) create mode 100644 test/options_test.rb create mode 100644 test/test_helper.rb diff --git a/Gemfile b/Gemfile index 526c34b..2d50b4f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,22 @@ source 'https://rubygems.org' +# The plugin itself (lib/, exe/) is pure Ruby with no gem dependencies, so that it can run inside ruby.wasm. + +# Gems used by the *generated* code (examples/ + end-to-end tests under tests/). gem 'connection_pool' gem 'mysql2' gem 'pg', '1.5.2' gem 'sqlite3' gem 'minitest' gem 'rake' -gem 'steep' \ No newline at end of file +gem 'steep' + +# Building dist/plugin.wasm (scripts/wasm/build.sh): provides `rbwasm pack`. +group :wasm_build do + gem 'ruby_wasm', '~> 2.7' +end + +# test/codec_test.rb only: independent protobuf implementation to cross-check the plugin's own codec. +group :codec_test do + gem 'google-protobuf', '~> 4.0' +end diff --git a/Gemfile.lock b/Gemfile.lock index 45e9b56..de96e20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,6 +22,12 @@ GEM ruby2_keywords ffi (1.16.3) fileutils (1.7.2) + google-protobuf (4.36.2) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.2-arm64-darwin) + bigdecimal + rake (~> 13.3) i18n (1.14.5) concurrent-ruby (~> 1.0) json (2.7.1) @@ -40,13 +46,17 @@ GEM pg (1.5.2) racc (1.7.3) rainbow (3.1.1) - rake (13.2.1) + rake (13.4.2) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) rbs (3.4.0) abbrev ruby2_keywords (0.0.5) + ruby_wasm (2.10.1) + logger + ruby_wasm (2.10.1-arm64-darwin) + logger securerandom (0.3.1) sqlite3 (2.0.2) mini_portile2 (~> 2.8.0) @@ -79,10 +89,12 @@ PLATFORMS DEPENDENCIES connection_pool + google-protobuf (~> 4.0) minitest mysql2 pg (= 1.5.2) rake + ruby_wasm (~> 2.7) sqlite3 steep diff --git a/ruby-wasm-poc/README.md b/docs/Design.md similarity index 100% rename from ruby-wasm-poc/README.md rename to docs/Design.md diff --git a/exe/plugin.rb b/exe/plugin.rb new file mode 100755 index 0000000..f4e970f --- /dev/null +++ b/exe/plugin.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# sqlc plugin entry point: GenerateRequest on stdin -> GenerateResponse on stdout. +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'sqlc_gen_ruby/generator' + +$stdin.binmode +$stdout.binmode +begin + request = SqlcGenRuby::Plugin::GenerateRequest.decode($stdin.read) + response = SqlcGenRuby::Generator.new.generate(request) + $stdout.write(response.encode) +rescue StandardError => e + # sqlc shows the plugin's stderr when it exits non-zero. + warn "sqlc-gen-ruby: #{e.class}: #{e.message}" + warn e.backtrace.first(10).join("\n") if ENV['SQLC_GEN_RUBY_DEBUG'] + exit 1 +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb b/lib/sqlc_gen_ruby/code.rb similarity index 93% rename from ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb rename to lib/sqlc_gen_ruby/code.rb index 69fa4e8..71b61f9 100644 --- a/ruby-wasm-poc/lib/sqlc_gen_ruby/code.rb +++ b/lib/sqlc_gen_ruby/code.rb @@ -30,6 +30,8 @@ def snake_case(str) end def upcase_first(str) + raise ArgumentError, 'input cannot be empty' if str.empty? + str[0].upcase + str[1..] end diff --git a/lib/sqlc_gen_ruby/drivers/base.rb b/lib/sqlc_gen_ruby/drivers/base.rb new file mode 100644 index 0000000..d6ba55a --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative '../code' + +module SqlcGenRuby + module Drivers + # Port of Drivers/DbDriver.cs (+ ColumnMappingConfig.cs, RowDataType.cs, Variable.cs). + # + # A driver supplies the DB-specific snippets; Generator/MethodGen assemble the file around them. + # Every `*_code` / `*_type` pair mirrors BuildCode / BuildType of the C# syntax-tree nodes. + class Base + ColumnMapping = Struct.new(:ruby_type, :db_types, :prefix_based) + + # Names of the generated variables (Variable.cs): snake_case(AsVar) / '@' + snake_case(AsProperty). + DB = '@db' + CLIENT = 'client' + STMT = 'stmt' + RESULT = 'result' + ROW = 'row' + ENTITY = 'entity' + ENTITIES = 'entities' + QUERY_PARAMS = 'query_params' + + # Name used for the generated module (`Codegen`) - same as the C# DriverName enum value. + def name = raise(NotImplementedError) + + def required_gems = ['connection_pool', *driver_gems] + + def init_code = raise(NotImplementedError) + + def init_type = raise(NotImplementedError) + + # RHS of the generated `Sql` constant (without the constant name). + def query_text(query) = query.text + + def prepare_stmt(_func_name, _const) = raise(NotImplementedError) + + def execute_stmt(_func_name, _has_params) = raise(NotImplementedError) + + # Whether generated methods borrow the client from a ConnectionPool (`@db.with do |client|`) + # or use the single injected connection (`client = @db`). + def pooling_enabled? = true + + # How rows are read: :hash => row['col'], :array => row[0] + def row_data_type = :hash + + def supports_exec_last_id? = false + + def column_type(column) + db_type = column.type.name.downcase # from sqlc - char(5), int4, float8, blob, ... + column_mappings.each do |mapping| + matched = if mapping.prefix_based + mapping.db_types.any? { |d| d.start_with?(db_type) } + else + mapping.db_types.include?(db_type) + end + return mapping.ruby_type if matched + end + raise ArgumentError, "Unsupported column type: #{column.type.name}" + end + + def query_text_constant(query) + const = Code.upcase_first("#{query.name}Sql") + Code.dedent_to_first_line("#{const} = %q(#{query_text(query)})") + end + + private + + def driver_gems = raise(NotImplementedError) + + def column_mappings = raise(NotImplementedError) + + def mapping(ruby_type, db_types, prefix_based: false) + ColumnMapping.new(ruby_type, db_types, prefix_based) + end + end + end +end diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb new file mode 100644 index 0000000..aac5f91 --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require_relative 'base' + +module SqlcGenRuby + module Drivers + # Port of Drivers/Mysql2Driver.cs. + class Mysql2 < Base + def name = 'Mysql2' + + def init_code + pool = "ConnectionPool.new(**connection_pool_params) {\n" \ + "#{Code.indent('Mysql2::Client.new(**mysql2_params)')}\n}" + Code.block('def initialize(connection_pool_params, mysql2_params)', "@db = #{pool}") + end + + def init_type + "@db: untyped\n" \ + 'def initialize: (Hash[String, String], Hash[String, String]) -> void' + end + + def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" + + def execute_stmt(_func_name, has_params) = has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' + + def supports_exec_last_id? = true + + private + + def driver_gems = %w[mysql2] + + def column_mappings + @column_mappings ||= [ + mapping('Array[Integer]', %w[binary bit blob longblob mediumblob tinyblob varbinary]), + mapping('String', %w[char date datetime decimal longtext mediumtext text time timestamp tinytext + varchar json]), + mapping('Integer', %w[bigint int mediumint smallint tinyint year]), + mapping('Float', %w[double float]) + ] + end + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb similarity index 53% rename from ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb rename to lib/sqlc_gen_ruby/drivers/pg.rb index d9dbc48..1c84acf 100644 --- a/ruby-wasm-poc/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -1,32 +1,13 @@ # frozen_string_literal: true -require_relative '../code' +require_relative 'base' module SqlcGenRuby module Drivers - # Port of Drivers/PgDriver.cs. Each *_code / *_type pair mirrors the - # BuildCode / BuildType of the C# syntax tree nodes. - class Pg - include Code - + # Port of Drivers/PgDriver.cs. + class Pg < Base def name = 'Pg' - def required_gems = %w[connection_pool pg set] - - COLUMN_TYPES = [ - ['bool', %w[bool boolean]], - ['Array[Integer]', %w[binary bit bytea blob longblob mediumblob tinyblob varbinary]], - ['String', %w[char date datetime longtext mediumtext text bpchar time timestamp tinytext varchar json]], - ['Integer', %w[int2 int4 int8 serial bigserial]], - ['Float', %w[numeric float4 float8 decimal]] - ].freeze - - def column_type(column) - db_type = column.type.name.downcase - COLUMN_TYPES.each { |ruby, dbs| return ruby if dbs.include?(db_type) } - raise "Unsupported column type: #{column.type.name}" - end - def init_code pool_body = [ 'client = PG.connect(**pg_params)', @@ -43,10 +24,10 @@ def init_type 'def initialize: (Hash[String, String], Hash[String, String]) -> void' end - def query_text_constant(query) + # sqlc emits `?` placeholders; PostgreSQL wants $1, $2, ... + def query_text(query) counter = 0 - text = query.text.gsub('?') { "$#{counter += 1}" } - Code.dedent_to_first_line("#{query.name}Sql = %q(#{text})") + query.text.gsub('?') { "$#{counter += 1}" } end def prepare_stmt(func_name, const) @@ -57,6 +38,21 @@ def prepare_stmt(func_name, const) def execute_stmt(func_name, has_params) "client.exec_prepared('#{func_name}'#{has_params ? ', query_params' : ''})" end + + private + + def driver_gems = %w[pg set] + + def column_mappings + @column_mappings ||= [ + mapping('bool', %w[bool boolean]), + mapping('Array[Integer]', %w[binary bit bytea blob longblob mediumblob tinyblob varbinary]), + mapping('String', %w[char date datetime longtext mediumtext text bpchar time timestamp tinytext + varchar json]), + mapping('Integer', %w[int2 int4 int8 serial bigserial]), + mapping('Float', %w[numeric float4 float8 decimal]) + ] + end end end end diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb new file mode 100644 index 0000000..2e0604d --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative 'base' + +module SqlcGenRuby + module Drivers + # Port of Drivers/Sqlite3Driver.cs. A single SQLite3::Database is injected, so there is no connection + # pool, and rows are read by index (the sqlite3 gem returns arrays by default). + class Sqlite3 < Base + def name = 'Sqlite3' + + def init_code = Code.block('def initialize(db)', '@db = db') + + def init_type = "@db: untyped\ndef initialize: (untyped) -> void" + + def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" + + def execute_stmt(_func_name, has_params) = has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' + + def pooling_enabled? = false + + def row_data_type = :array + + private + + def driver_gems = %w[sqlite3] + + def column_mappings + @column_mappings ||= [ + mapping('Integer', %w[int integer tinyint smallint mediumint bigint unsignedbigint int2 int8]), + mapping('bool', %w[boolean bool]), + mapping('Array[Integer]', %w[blob]), + mapping('String', %w[character varchar varyingcharacter nchar nativecharacter nvarchar text clob], + prefix_based: true), + mapping('Float', %w[real double doubleprecision float decimal numeric], prefix_based: true) + ] + end + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb b/lib/sqlc_gen_ruby/generator.rb similarity index 66% rename from ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb rename to lib/sqlc_gen_ruby/generator.rb index 4201f6d..3e30954 100644 --- a/ruby-wasm-poc/lib/sqlc_gen_ruby/generator.rb +++ b/lib/sqlc_gen_ruby/generator.rb @@ -3,7 +3,10 @@ require_relative 'code' require_relative 'options' require_relative 'plugin_pb' +require_relative 'method_gen' require_relative 'drivers/pg' +require_relative 'drivers/mysql2' +require_relative 'drivers/sqlite3' module SqlcGenRuby # Ruby port of CodeGenerator/CodeGenerator.cs + Drivers/MethodGen.cs. @@ -25,15 +28,19 @@ def generate(request) private def instantiate_driver - case @options.driver.downcase + case @options.driver_name + when 'mysql2' then Drivers::Mysql2.new when 'pg' then Drivers::Pg.new - else raise ArgumentError, "unknown or not-yet-ported driver: #{@options.driver}" + when 'sqlite3' then Drivers::Sqlite3.new + else raise ArgumentError, "unknown driver: #{@options.driver_name}" end end # query.sql => QuerySql def class_name_for(filename) ext = File.extname(filename) + raise ArgumentError, "query file #{filename} has no extension" if ext.empty? + Code.upcase_first(File.basename(filename, ext)) + Code.upcase_first(ext[1..]) end @@ -67,12 +74,16 @@ def query_members(query) end def class_members(queries) - [[@driver.init_code, @driver.init_type]] + queries.map { |q| [method_code(q), method_type(q)] } + method_gen = MethodGen.new(@driver) + [[@driver.init_code, @driver.init_type]] + queries.map { |q| [method_gen.code(q), method_gen.type(q)] } end def dataclass(name, columns) pairs = columns.map { |c| [c.name, @driver.column_type(c)] } - @options.immutable_data_supported? ? data_define(name, pairs) : new_struct(name, pairs) + dupes = pairs.map(&:first).tally.select { |_, n| n > 1 }.keys + raise ArgumentError, "duplicate column names in #{name}: #{dupes.join(', ')}" unless dupes.empty? + + @options.ruby_version.immutable_data_supported? ? data_define(name, pairs) : new_struct(name, pairs) end def data_define(name, pairs) @@ -93,55 +104,6 @@ def new_struct(name, pairs) [code, "class #{name} < ::Struct\n#{accessors}\n#{init}\nend"] end - # --- methods (MethodGen) --- - - def method_code(query) - func = Code.snake_case(query.name) - const = "#{query.name}Sql" - args_var = Code.snake_case("#{query.name}Args") - has_params = !query.params.empty? - lines = [] - if has_params - list = Code.comma_join(query.params.map { |p| "#{args_var}.#{p.column.name}" }) - lines << "query_params = [#{list}]" - end - lines << @driver.prepare_stmt(func, const) - exec = @driver.execute_stmt(func, has_params) - - case query.cmd - when ':one' - lines << "result = #{exec}" - lines << 'row = result.first' - lines << 'return nil if row.nil?' - lines << "entity = #{new_row(query)}" - lines << 'return entity' - when ':many' - lines << "result = #{exec}" - lines << 'entities = []' - lines << Code.block('result.each do |row|', "entities << #{new_row(query)}") - lines << 'return entities' - when ':exec' - lines << exec - else - raise "unsupported query command: #{query.cmd}" - end - - params = has_params ? "(#{args_var})" : '' - Code.block("def #{func}#{params}", Code.block('@db.with do |client|', lines.join("\n"))) - end - - def new_row(query) - args = Code.comma_join(query.columns.map { |c| "row['#{c.name}']" }) - "#{query.name}Row.new(#{args})" - end - - def method_type(query) - func = Code.snake_case(query.name) - params = query.params.empty? ? '' : "(#{query.name}Args) " - ret = query.cmd == ':one' ? "#{query.name}Row?" : 'void' - "\ndef #{func}: #{params}-> #{ret}" - end - def gemfile gems = @driver.required_gems.map { |g| "gem '#{g}'" }.join("\n") Plugin::File.new(name: 'Gemfile', contents: "source 'https://rubygems.org'\n\n#{gems}") diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb new file mode 100644 index 0000000..51d5031 --- /dev/null +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require_relative 'code' + +module SqlcGenRuby + # Port of Drivers/MethodGen.cs: builds the code (and RBS signature) of one generated query method, + # parameterised by the driver's pooling / row-access / statement flavour. + class MethodGen + def initialize(driver) + @driver = driver + end + + def code(query) + func = Code.snake_case(query.name) + args_var = Code.snake_case("#{query.name}Args") + has_params = !query.params.empty? + + statements = [] + statements << query_params(args_var, query.params) if has_params + statements << @driver.prepare_stmt(func, "#{query.name}Sql") + statements.concat(command_statements(query, func, has_params)) + + params = has_params ? "(#{args_var})" : '' + Code.block("def #{func}#{params}", method_body(statements, query.cmd)) + end + + def type(query) + func = Code.snake_case(query.name) + params = query.params.empty? ? '' : "(#{query.name}Args) " + "\ndef #{func}: #{params}-> #{return_type(query)}" + end + + private + + def return_type(query) + case query.cmd + when ':one' then "#{query.name}Row?" + when ':execlastid' then 'Integer' + else 'void' + end + end + + def query_params(args_var, params) + list = Code.comma_join(params.map { |p| "#{args_var}.#{p.column.name}" }) + "query_params = [#{list}]" + end + + def command_statements(query, func, has_params) + exec = @driver.execute_stmt(func, has_params) + case query.cmd + when ':one' + ["result = #{exec}", + 'row = result.first', + 'return nil if row.nil?', + "entity = #{new_row(query)}", + 'return entity'] + when ':many' + ["result = #{exec}", + 'entities = []', + Code.block('result.each do |row|', "entities << #{new_row(query)}"), + 'return entities'] + when ':exec' + [exec] + when ':execlastid' + unless @driver.supports_exec_last_id? + raise ArgumentError, "the #{query.cmd} command is not supported by the #{@driver.name} driver" + end + + [exec, 'return client.last_id'] + else + raise ArgumentError, "unsupported query command: #{query.cmd}" + end + end + + # :execlastid is always pooled (the C# ExecLastIdDeclare had no non-pooled variant). + def method_body(statements, cmd) + body = statements.join("\n") + return Code.block('@db.with do |client|', body) if @driver.pooling_enabled? || cmd == ':execlastid' + + "client = @db\n#{body}" + end + + def new_row(query) + args = Code.comma_join(query.columns.each_with_index.map { |c, i| row_access(c, i) }) + "#{query.name}Row.new(#{args})" + end + + def row_access(column, index) + @driver.row_data_type == :hash ? "row['#{column.name}']" : "row[#{index}]" + end + end +end diff --git a/lib/sqlc_gen_ruby/options.rb b/lib/sqlc_gen_ruby/options.rb new file mode 100644 index 0000000..b02c0a1 --- /dev/null +++ b/lib/sqlc_gen_ruby/options.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'json' +require_relative 'ruby_version' + +module SqlcGenRuby + # Port of PluginOptions/Options.cs + RawOptions.cs: parses the JSON `options:` block of the sqlc config. + class Options + DRIVERS = %w[mysql2 pg sqlite3].freeze + + attr_reader :driver_name, :ruby_version, :generate_gemfile, :generate_types + + def initialize(plugin_options_bytes) + text = plugin_options_bytes.to_s.dup.force_encoding(Encoding::UTF_8) + raw = JSON.parse(text) + raise ArgumentError, 'plugin options must be a JSON object' unless raw.is_a?(Hash) + + @driver_name = fetch_string(raw, 'driver', required: true).downcase + raise ArgumentError, "unknown driver: #{raw['driver']}" unless DRIVERS.include?(@driver_name) + + @generate_gemfile = fetch_bool(raw, 'generateGemfile', false) # not generating Gemfile by default + @generate_types = fetch_bool(raw, 'generateTypes', true) + @ruby_version = RubyVersion.parse(fetch_string(raw, 'rubyVersion', default: '3.3')) + end + + private + + def fetch_string(raw, key, default: nil, required: false) + raise ArgumentError, "missing required option: #{key}" if required && !raw.key?(key) + + value = raw.fetch(key, default) + raise ArgumentError, "option #{key} must be a string" unless value.is_a?(String) + + value + end + + def fetch_bool(raw, key, default) + value = raw.fetch(key, default) + raise ArgumentError, "option #{key} must be a boolean" unless [true, false].include?(value) + + value + end + end +end diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb b/lib/sqlc_gen_ruby/plugin_pb.rb similarity index 100% rename from ruby-wasm-poc/lib/sqlc_gen_ruby/plugin_pb.rb rename to lib/sqlc_gen_ruby/plugin_pb.rb diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb b/lib/sqlc_gen_ruby/proto.rb similarity index 99% rename from ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb rename to lib/sqlc_gen_ruby/proto.rb index 48530d9..afa60e5 100644 --- a/ruby-wasm-poc/lib/sqlc_gen_ruby/proto.rb +++ b/lib/sqlc_gen_ruby/proto.rb @@ -94,6 +94,8 @@ def initialize(data) @pos = 0 end + attr_reader :pos + def eof? @pos >= @data.bytesize end diff --git a/lib/sqlc_gen_ruby/ruby_version.rb b/lib/sqlc_gen_ruby/ruby_version.rb new file mode 100644 index 0000000..763a18b --- /dev/null +++ b/lib/sqlc_gen_ruby/ruby_version.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module SqlcGenRuby + # Port of PluginOptions/RubyVersion.cs. The comparison deliberately mirrors the C# record's + # CompareTo, including its handling of omitted minor / patch parts (e.g. "3" compares equal to any + # "3.x", while "3.3.5" compares greater than "3.3", so it is rejected by the max-version check). + class RubyVersion + include Comparable + + SEMVER = /\A\s*(0|[1-9][0-9]*)(?:\.(0|[1-9][0-9]*))?(?:\.(0|[1-9][0-9]*))?\s*\z/.freeze + SHORT_MAX = 32_767 # the C# implementation parses each part as a `short` + + attr_reader :major, :minor, :patch + + def initialize(major, minor = nil, patch = nil) + @major = major + @minor = minor + @patch = patch + end + + def self.parse(pattern) + version = parse_semantic(pattern) + if version < MIN_SUPPORTED + raise ArgumentError, "Provided version #{version} exceeds min version #{MIN_SUPPORTED}" + end + if version > MAX_SUPPORTED + raise ArgumentError, "Provided version #{version} exceeds max version #{MAX_SUPPORTED}" + end + + version + end + + def self.parse_semantic(pattern) + match = SEMVER.match(pattern.to_s) or + raise ArgumentError, "version #{pattern} can't be parsed to a semantic version" + parts = match.captures.map { |c| c && short(c) } + new(*parts) + end + + def self.short(text) + n = Integer(text, 10) + raise ArgumentError, "version part #{text} is out of range" if n > SHORT_MAX + + n + end + + def <=>(other) + return 1 if other.nil? + + cmp = major <=> other.major + return cmp unless cmp.zero? + return cmp if minor.nil? + + cmp = compare_nullable(minor, other.minor) + return cmp unless cmp.zero? + return cmp if patch.nil? + + compare_nullable(patch, other.patch) + end + + # Records compare by value in C# (`==`), unlike CompareTo which treats missing parts as "equal". + def ==(other) + other.is_a?(RubyVersion) && [major, minor, patch] == [other.major, other.minor, other.patch] + end + alias eql? == + + def hash = [major, minor, patch].hash + + # Ruby 3.2 introduced Data.define; older versions have to fall back to Struct. + def immutable_data_supported? + self >= IMMUTABLE_DATA_MIN + end + + def to_s + [major, minor, patch].compact.join('.') + end + + # int.CompareTo(int?): a missing value sorts before any number. + def compare_nullable(value, other) + other.nil? ? 1 : value <=> other + end + private :compare_nullable + + MIN_SUPPORTED = new(3, 1).freeze + MAX_SUPPORTED = new(3, 3).freeze + IMMUTABLE_DATA_MIN = new(3, 2).freeze + end +end diff --git a/ruby-wasm-poc/Gemfile b/ruby-wasm-poc/Gemfile deleted file mode 100644 index b370ee9..0000000 --- a/ruby-wasm-poc/Gemfile +++ /dev/null @@ -1,11 +0,0 @@ -source 'https://rubygems.org' - -# Build-time only: used to pack the plugin into ruby.wasm. -gem 'ruby_wasm', '~> 2.7' - -group :test do - # Test-time only (independent protobuf encoder/decoder used by test/fixture.rb). - # NOT used by the plugin itself - C extensions can't be loaded by ruby.wasm. - gem 'google-protobuf', '~> 4.0' - gem 'minitest' -end diff --git a/ruby-wasm-poc/exe/plugin.rb b/ruby-wasm-poc/exe/plugin.rb deleted file mode 100644 index b6785b9..0000000 --- a/ruby-wasm-poc/exe/plugin.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -# sqlc plugin entry point: GenerateRequest on stdin -> GenerateResponse on stdout. -$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) -require 'sqlc_gen_ruby/generator' - -$stdin.binmode -$stdout.binmode -request = SqlcGenRuby::Plugin::GenerateRequest.decode($stdin.read) -response = SqlcGenRuby::Generator.new.generate(request) -$stdout.write(response.encode) diff --git a/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb b/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb deleted file mode 100644 index ea7366c..0000000 --- a/ruby-wasm-poc/lib/sqlc_gen_ruby/options.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'json' - -module SqlcGenRuby - class Options - attr_reader :driver, :ruby_version, :generate_gemfile, :generate_types - - def initialize(plugin_options_bytes) - text = plugin_options_bytes.to_s.dup.force_encoding(Encoding::UTF_8) - raw = text.strip.empty? ? {} : JSON.parse(text) - @driver = raw.fetch('driver') - @generate_gemfile = raw.fetch('generateGemfile', false) - @generate_types = raw.fetch('generateTypes', true) - @ruby_version = Gem::Version.new(raw.fetch('rubyVersion', '3.3')) - end - - def immutable_data_supported? - ruby_version >= Gem::Version.new('3.2') - end - end -end diff --git a/ruby-wasm-poc/test/golden_test.rb b/ruby-wasm-poc/test/golden_test.rb deleted file mode 100644 index ace62ea..0000000 --- a/ruby-wasm-poc/test/golden_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -# Runs the real plugin entry point (exe/plugin.rb) as a subprocess over stdin/stdout -# exactly the way sqlc runs it, then diffs the output with the C# plugin's output -# checked in under examples/pg. -require 'open3' -require 'minitest/autorun' -require_relative 'fixture' - -class GoldenTest < Minitest::Test - ROOT = File.expand_path('..', __dir__) - EXAMPLES = File.expand_path('../../examples/pg', __dir__) - - def setup - out, err, st = Open3.capture3('ruby', File.join(ROOT, 'exe/plugin.rb'), - stdin_data: Fixture.k('GenerateRequest').encode(Fixture.request), binmode: true) - assert st.success?, err - @files = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } - end - - def normalize(s) = s.delete("\r").strip - - %w[query_sql.rb query_sql.rbs].each do |name| - define_method("test_#{name}_matches_csharp_output") do - assert_equal normalize(File.read(File.join(EXAMPLES, name))), normalize(@files.fetch(name)) - end - end -end diff --git a/scripts/capture_request.rb b/scripts/capture_request.rb new file mode 100755 index 0000000..1df6442 --- /dev/null +++ b/scripts/capture_request.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# A sqlc *process plugin* that records the GenerateRequest sqlc sends it, to refresh test/fixtures/*.bin. +# The (large, unused) catalog field is stripped; every other byte is exactly what sqlc sent. +# +# cat > /tmp/capture-pg.sh <<'SH' +# #!/bin/sh +# exec ruby /path/to/scripts/capture_request.rb /path/to/test/fixtures/pg.bin +# SH +# chmod +x /tmp/capture-pg.sh # then point a sqlc process plugin `cmd:` at it and run `sqlc generate` +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'sqlc_gen_ruby/proto' + +CATALOG_FIELD = 2 + +data = $stdin.binmode.read +reader = SqlcGenRuby::Proto::Reader.new(data) +out = ''.b +until reader.eof? + start = reader.pos + number, wire = reader.read_tag + reader.skip(wire) + out << data.byteslice(start, reader.pos - start) unless number == CATALOG_FIELD +end +File.binwrite(ARGV.fetch(0), out) +# sqlc expects a (possibly empty) GenerateResponse back +$stdout.binmode.write('') diff --git a/ruby-wasm-poc/scripts/build_wasm.sh b/scripts/wasm/build.sh similarity index 97% rename from ruby-wasm-poc/scripts/build_wasm.sh rename to scripts/wasm/build.sh index 4511463..df4d392 100755 --- a/ruby-wasm-poc/scripts/build_wasm.sh +++ b/scripts/wasm/build.sh @@ -2,8 +2,8 @@ # Packs the pure-Ruby plugin into a standalone WASI module (dist/plugin.wasm). # Needs: ruby + `gem install ruby_wasm` (for `rbwasm pack`), curl, tar. set -euo pipefail -cd "$(dirname "$0")/.." -PLUGIN_DIR="$(pwd)" +cd "$(dirname "$0")/../.." + RUBY_WASM_VERSION="${RUBY_WASM_VERSION:-2.7.1}" FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" diff --git a/test/codec_test.rb b/test/codec_test.rb new file mode 100644 index 0000000..09848af --- /dev/null +++ b/test/codec_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Cross-checks the plugin's hand-written protobuf codec against Google's official implementation +# (test-only dependency; the plugin itself is pure Ruby so it can run inside ruby.wasm). +begin + require_relative 'official_protobuf' +rescue LoadError + warn 'google-protobuf not installed - skipping codec cross-check' + return +end +require_relative 'test_helper' + +class CodecTest < Minitest::Test + include PluginHelper + + def test_official_request_decodes_and_generates_same_output_as_captured_one + official = Fixture.k('GenerateRequest').encode(Fixture.request) + out, err, status = run_plugin(official) + assert status.success?, err + files = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } + %w[query_sql.rb query_sql.rbs].each do |name| + assert_equal normalize(File.read(File.join(ROOT, 'examples/pg', name))), normalize(files.fetch(name)) + end + end + + def test_captured_sqlc_request_roundtrips_through_the_codec + bytes = File.binread(File.join(FIXTURES, 'pg.bin')) + decoded = Plugin::GenerateRequest.decode(bytes) + official = Fixture.k('GenerateRequest').decode(Fixture.k('GenerateRequest').encode(Fixture.request)) + assert_equal official.queries.map(&:name), %w[GetAuthor ListAuthors CreateAuthor DeleteAuthor Test] + refute_empty decoded.queries + assert_equal decoded.queries.size, Plugin::GenerateRequest.decode(decoded.encode).queries.size + end +end diff --git a/test/extended_golden_test.rb b/test/extended_golden_test.rb new file mode 100644 index 0000000..38bb8c8 --- /dev/null +++ b/test/extended_golden_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +# Snapshots of the C# plugin's output (recorded while it still existed) for the sqlc-captured queries plus +# synthetic ones (5+ params/columns, lower-case names, several query files) under different options: +# Ruby 3.1 (Struct) / 3.2 / generateGemfile / generateTypes=false. +class ExtendedGoldenTest < Minitest::Test + include PluginHelper + + GOLDEN = File.join(PluginHelper::ROOT, 'test/golden') + + Dir.glob(File.join(GOLDEN, '*/*/')).sort.each do |dir| + driver_case = File.basename(File.dirname(dir)) + variant = File.basename(dir) + define_method("test_#{driver_case}_#{variant}") do + options = JSON.parse(File.read(File.join(dir, 'options.json'))) + files = generate(load_request(driver_case, **options.transform_keys(&:to_sym))) + expected = Dir.children(dir).reject { |f| f == 'options.json' }.sort + assert_equal expected, files.keys.sort + expected.each { |name| assert_equal File.binread(File.join(dir, name)), files.fetch(name), name } + end + end +end diff --git a/test/fixtures/mysql2.bin b/test/fixtures/mysql2.bin new file mode 100644 index 0000000..18c25b4 --- /dev/null +++ b/test/fixtures/mysql2.bin @@ -0,0 +1,100 @@ + +ý +2mysql!examples/authors/mysql/schema.sql" examples/authors/mysql/query.sqlb« +e/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/out_mysql2rubyT{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"}*f +d/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/mysql2.shˆ +6SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 GetAuthor:one"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlÔ +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlÓ +6INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) CreateAuthor:exec*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authors° +(UPDATE authors +SET bio = ? +WHERE id = ? UpdateAuthor:exec*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlä +9INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +)CreateAuthorReturnId :execlastid*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsr + DELETE FROM authors +WHERE id = ? DeleteAuthor:exec*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sql´ +­SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types +LIMIT 1Test:one"+ +c_bit0Rnode_mysql_typesbbitzc_bit"@ + c_tinyint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb tinyintz c_tinyint"1 +c_bool0Rnode_mysql_typesb tinyintzc_bool"7 + c_boolean0Rnode_mysql_typesb tinyintz c_boolean"C + +c_smallint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +smallintz +c_smallint"F + c_mediumint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  mediumintz c_mediumint"4 +c_int0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintzc_int"< + c_integer0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintz c_integer"= +c_bigint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_bigint"B +c_serial0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_serial€"7 + c_decimal0Rnode_mysql_typesb decimalz c_decimal"/ +c_dec0Rnode_mysql_typesb decimalzc_dec"7 + c_numeric0Rnode_mysql_typesb decimalz c_numeric"3 +c_fixed0Rnode_mysql_typesb decimalzc_fixed": +c_float0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbfloatzc_float"= +c_double0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double"Q +c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double_precision"7 +c_date0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdatezc_date". +c_time0 +Rnode_mysql_typesbtimezc_time": + +c_datetime0Rnode_mysql_typesb +datetimez +c_datetime"= + c_timestamp0Rnode_mysql_typesb  timestampz c_timestamp"7 +c_year0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbyearzc_year"7 +c_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_char"9 +c_nchar0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_nchar"I +c_national_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_national_char"7 + c_varchar0 +Rnode_mysql_typesb varcharz c_varchar"= +c_binary0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbinaryzc_binary"= + c_varbinary0 +Rnode_mysql_typesb  varbinaryz c_varbinary"C + +c_tinyblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinyblobz +c_tinyblob"C + +c_tinytext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinytextz +c_tinytext"7 +c_blob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbblobzc_blob"7 +c_text0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbtextzc_text"I + c_mediumblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumblobz c_mediumblob"I + c_mediumtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumtextz c_mediumtext"C + +c_longblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longblobz +c_longblob"C + +c_longtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longtextz +c_longtext"7 +c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjsonzc_json: query.sql"v1.27.0*T{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/mysql2_extended.bin b/test/fixtures/mysql2_extended.bin new file mode 100644 index 0000000..9ab0eb5 --- /dev/null +++ b/test/fixtures/mysql2_extended.bin @@ -0,0 +1,133 @@ +ˆ +6SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 GetAuthor:one"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlÔ +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlÓ +6INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) CreateAuthor:exec*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authors° +(UPDATE authors +SET bio = ? +WHERE id = ? UpdateAuthor:exec*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlä +9INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +)CreateAuthorReturnId :execlastid*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsr + DELETE FROM authors +WHERE id = ? DeleteAuthor:exec*.* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sql´ +­SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types +LIMIT 1Test:one"+ +c_bit0Rnode_mysql_typesbbitzc_bit"@ + c_tinyint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb tinyintz c_tinyint"1 +c_bool0Rnode_mysql_typesb tinyintzc_bool"7 + c_boolean0Rnode_mysql_typesb tinyintz c_boolean"C + +c_smallint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +smallintz +c_smallint"F + c_mediumint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  mediumintz c_mediumint"4 +c_int0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintzc_int"< + c_integer0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintz c_integer"= +c_bigint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_bigint"B +c_serial0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_serial€"7 + c_decimal0Rnode_mysql_typesb decimalz c_decimal"/ +c_dec0Rnode_mysql_typesb decimalzc_dec"7 + c_numeric0Rnode_mysql_typesb decimalz c_numeric"3 +c_fixed0Rnode_mysql_typesb decimalzc_fixed": +c_float0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbfloatzc_float"= +c_double0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double"Q +c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double_precision"7 +c_date0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdatezc_date". +c_time0 +Rnode_mysql_typesbtimezc_time": + +c_datetime0Rnode_mysql_typesb +datetimez +c_datetime"= + c_timestamp0Rnode_mysql_typesb  timestampz c_timestamp"7 +c_year0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbyearzc_year"7 +c_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_char"9 +c_nchar0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_nchar"I +c_national_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_national_char"7 + c_varchar0 +Rnode_mysql_typesb varcharz c_varchar"= +c_binary0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbinaryzc_binary"= + c_varbinary0 +Rnode_mysql_typesb  varbinaryz c_varbinary"C + +c_tinyblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinyblobz +c_tinyblob"C + +c_tinytext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinytextz +c_tinytext"7 +c_blob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbblobzc_blob"7 +c_text0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbtextzc_text"I + c_mediumblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumblobz c_mediumblob"I + c_mediumtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumtextz c_mediumtext"C + +c_longblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longblobz +c_longblob"C + +c_longtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longtextz +c_longtext"7 +c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjsonzc_json: query.sqlÊ +SELECT 1 -- ? ? + WHERE a = ? +ManyParams:one" +c0bint" +c1b varchar* +c0bint* +c1b varchar* +c2bdouble* +c3bblob* +c4b tinyint: query.sql¦ +SELECT 1 -- ? ? + WHERE a = ?manyRows:many" +c0bint" +c1b varchar" +c2bdouble" +c3bblob" +c4b tinyint* +c0bint: query.sql€ +SELECT 1 -- ? ? + WHERE a = ?FourCols:many" +c0bint" +c1b varchar" +c2bdouble" +c3bblob: query.sql¥ +SELECT 1 -- ? ? + WHERE a = ?NoCols:exec* +c0bint* +c1b varchar* +c2bdouble* +c3bblob* +c4b tinyint: other.sqlJ +SELECT 1 -- ? ? + WHERE a = ?OtherOne:one" +c0bint: other.sqlZ +SELECT 1 -- ? ? + WHERE a = ?WithNumbers2Go:exec* +c0bint:my_queries.sql"v1.27.0*T{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/pg.bin b/test/fixtures/pg.bin new file mode 100644 index 0000000..2a907b7 --- /dev/null +++ b/test/fixtures/pg.bin @@ -0,0 +1,66 @@ + +€ +2 +postgresql&examples/authors/postgresql/schema.sql"%examples/authors/postgresql/query.sqlbŸ +a/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/out_pgrubyP{"driver":"pg","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"}*b +`/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/pg.sh� +7SELECT id, name, bio FROM authors +WHERE id = $1 LIMIT 1 GetAuthor:one"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*1- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql× +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlö +SINSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING id, name, bio CreateAuthor:one"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsv +!DELETE FROM authors +WHERE id = $1 DeleteAuthor:exec*1- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql² +ÞSELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types +LIMIT 1Test:one"C +c_bit0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogbitzc_bit"N + +c_smallint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint2z +c_smallint"L + c_boolean0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogboolz c_boolean"L + c_integer0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint4z c_integer"J +c_bigint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint8zc_bigint"@ +c_serial0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbserialzc_serial"O + c_decimal0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalognumericz c_decimal"O + c_numeric0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalognumericz c_numeric"H +c_real0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogfloat4zc_real"` +c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogfloat8zc_double_precision": +c_date0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbdatezc_date"F +c_time0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogtimezc_time"U + c_timestamp0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalog timestampz c_timestamp"H +c_char0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogbpcharzc_char"O + c_varchar0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogvarcharz c_varchar"= +c_bytea0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbbyteazc_bytea": +c_text0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbtextzc_text": +c_json0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbjsonzc_json: query.sql"v1.27.0*P{"driver":"pg","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/pg_extended.bin b/test/fixtures/pg_extended.bin new file mode 100644 index 0000000..a98a0a2 --- /dev/null +++ b/test/fixtures/pg_extended.bin @@ -0,0 +1,98 @@ +� +7SELECT id, name, bio FROM authors +WHERE id = $1 LIMIT 1 GetAuthor:one"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*1- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql× +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlö +SINSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING id, name, bio CreateAuthor:one"- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsv +!DELETE FROM authors +WHERE id = $1 DeleteAuthor:exec*1- +id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql² +ÞSELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types +LIMIT 1Test:one"C +c_bit0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogbitzc_bit"N + +c_smallint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint2z +c_smallint"L + c_boolean0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogboolz c_boolean"L + c_integer0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint4z c_integer"J +c_bigint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogint8zc_bigint"@ +c_serial0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbserialzc_serial"O + c_decimal0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalognumericz c_decimal"O + c_numeric0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalognumericz c_numeric"H +c_real0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogfloat4zc_real"` +c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogfloat8zc_double_precision": +c_date0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbdatezc_date"F +c_time0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogtimezc_time"U + c_timestamp0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalog timestampz c_timestamp"H +c_char0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogbpcharzc_char"O + c_varchar0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb +pg_catalogvarcharz c_varchar"= +c_bytea0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbbyteazc_bytea": +c_text0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbtextzc_text": +c_json0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbjsonzc_json: query.sqlÄ +SELECT 1 -- ? ? + WHERE a = ? +ManyParams:one" +c0bbool" +c1bint4* +c0bbool* +c1bint4* +c2btext* +c3bfloat8* +c4bbytea: query.sql£ +SELECT 1 -- ? ? + WHERE a = ?manyRows:many" +c0bbool" +c1bint4" +c2btext" +c3bfloat8" +c4bbytea* +c0bbool: query.sql~ +SELECT 1 -- ? ? + WHERE a = ?FourCols:many" +c0bbool" +c1bint4" +c2btext" +c3bfloat8: query.sql¡ +SELECT 1 -- ? ? + WHERE a = ?NoCols:exec* +c0bbool* +c1bint4* +c2btext* +c3bfloat8* +c4bbytea: other.sqlK +SELECT 1 -- ? ? + WHERE a = ?OtherOne:one" +c0bbool: other.sql[ +SELECT 1 -- ? ? + WHERE a = ?WithNumbers2Go:exec* +c0bbool:my_queries.sql"v1.27.0*P{"driver":"pg","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/sqlite3.bin b/test/fixtures/sqlite3.bin new file mode 100644 index 0000000..6873373 --- /dev/null +++ b/test/fixtures/sqlite3.bin @@ -0,0 +1,26 @@ + +ƒ +2sqlite"examples/authors/sqlite/schema.sql"!examples/authors/sqlite/query.sqlb® +f/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/out_sqlite3rubyU{"driver":"sqlite3","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"}*g +e/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/sqlite3.shŠ +6SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 GetAuthor:one"+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*/+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid: query.sqlÕ +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlÎ +5INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) CreateAuthor:exec*62 +name0ÿÿÿÿÿÿÿÿÿRmainauthorsbtextzname*2. +bio0ÿÿÿÿÿÿÿÿÿRmainauthorsbtextzbio: query.sqlB authorss + DELETE FROM authors +WHERE id = ? DeleteAuthor:exec*/+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid: query.sql"v1.27.0*U{"driver":"sqlite3","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/sqlite3_extended.bin b/test/fixtures/sqlite3_extended.bin new file mode 100644 index 0000000..b5985e1 --- /dev/null +++ b/test/fixtures/sqlite3_extended.bin @@ -0,0 +1,59 @@ +Š +6SELECT id, name, bio FROM authors +WHERE id = ? LIMIT 1 GetAuthor:one"+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*/+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid: query.sqlÕ +/SELECT id, name, bio FROM authors +ORDER BY name ListAuthors:many"+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlÎ +5INSERT INTO authors ( + name, bio +) VALUES ( + ?, ? +) CreateAuthor:exec*62 +name0ÿÿÿÿÿÿÿÿÿRmainauthorsbtextzname*2. +bio0ÿÿÿÿÿÿÿÿÿRmainauthorsbtextzbio: query.sqlB authorss + DELETE FROM authors +WHERE id = ? DeleteAuthor:exec*/+ +id0ÿÿÿÿÿÿÿÿÿR authorsb INTEGERzid: query.sqlÊ +SELECT 1 -- ? ? + WHERE a = ? +ManyParams:one" +c0b integer" +c1btext* +c0b integer* +c1btext* +c2breal* +c3bblob* +c4b boolean: query.sql© +SELECT 1 -- ? ? + WHERE a = ?manyRows:many" +c0b integer" +c1btext" +c2breal" +c3bblob" +c4b boolean* +c0b integer: query.sql +SELECT 1 -- ? ? + WHERE a = ?FourCols:many" +c0b integer" +c1btext" +c2breal" +c3bblob: query.sql¤ +SELECT 1 -- ? ? + WHERE a = ?NoCols:exec* +c0b integer* +c1btext* +c2breal* +c3bblob* +c4b boolean: other.sqlN +SELECT 1 -- ? ? + WHERE a = ?OtherOne:one" +c0b integer: other.sql^ +SELECT 1 -- ? ? + WHERE a = ?WithNumbers2Go:exec* +c0b integer:my_queries.sql"v1.27.0*U{"driver":"sqlite3","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/my_queries_sql.rb b/test/golden/mysql2_extended/default/my_queries_sql.rb new file mode 100644 index 0000000..ed1a6a3 --- /dev/null +++ b/test/golden/mysql2_extended/default/my_queries_sql.rb @@ -0,0 +1,27 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/my_queries_sql.rbs b/test/golden/mysql2_extended/default/my_queries_sql.rbs new file mode 100644 index 0000000..fb8bee0 --- /dev/null +++ b/test/golden/mysql2_extended/default/my_queries_sql.rbs @@ -0,0 +1,16 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + class My_queriesSql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/options.json b/test/golden/mysql2_extended/default/options.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/test/golden/mysql2_extended/default/options.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/golden/mysql2_extended/default/other_sql.rb b/test/golden/mysql2_extended/default/other_sql.rb new file mode 100644 index 0000000..5853c6b --- /dev/null +++ b/test/golden/mysql2_extended/default/other_sql.rb @@ -0,0 +1,56 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/other_sql.rbs b/test/golden/mysql2_extended/default/other_sql.rbs new file mode 100644 index 0000000..1f60863 --- /dev/null +++ b/test/golden/mysql2_extended/default/other_sql.rbs @@ -0,0 +1,35 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + NoColsSql: String + + class NoColsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> void + end + + OtherOneSql: String + + class OtherOneRow + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + class OtherSql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/query_sql.rb b/test/golden/mysql2_extended/default/query_sql.rb new file mode 100644 index 0000000..d7d0d13 --- /dev/null +++ b/test/golden/mysql2_extended/default/query_sql.rb @@ -0,0 +1,294 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Data.define(:bio, :id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + stmt.execute(*query_params) + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + stmt.execute(*query_params) + return client.last_id + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/query_sql.rbs b/test/golden/mysql2_extended/default/query_sql.rbs new file mode 100644 index 0000000..2f3f7e2 --- /dev/null +++ b/test/golden/mysql2_extended/default/query_sql.rbs @@ -0,0 +1,217 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String + def initialize: ( String name, String bio) -> void + end + + UpdateAuthorSql: String + + class UpdateAuthorArgs + attr_reader bio: String + attr_reader id: Integer + def initialize: ( String bio, Integer id) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String + def initialize: ( String name, String bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + TestSql: String + + class TestRow + attr_reader c_bit: Array[Integer] + attr_reader c_tinyint: Integer + attr_reader c_bool: Integer + attr_reader c_boolean: Integer + attr_reader c_smallint: Integer + attr_reader c_mediumint: Integer + attr_reader c_int: Integer + attr_reader c_integer: Integer + attr_reader c_bigint: Integer + attr_reader c_serial: Integer + attr_reader c_decimal: String + attr_reader c_dec: String + attr_reader c_numeric: String + attr_reader c_fixed: String + attr_reader c_float: Float + attr_reader c_double: Float + attr_reader c_double_precision: Float + attr_reader c_date: String + attr_reader c_time: String + attr_reader c_datetime: String + attr_reader c_timestamp: String + attr_reader c_year: Integer + attr_reader c_char: String + attr_reader c_nchar: String + attr_reader c_national_char: String + attr_reader c_varchar: String + attr_reader c_binary: Array[Integer] + attr_reader c_varbinary: Array[Integer] + attr_reader c_tinyblob: Array[Integer] + attr_reader c_tinytext: String + attr_reader c_blob: Array[Integer] + attr_reader c_text: String + attr_reader c_mediumblob: Array[Integer] + attr_reader c_mediumtext: String + attr_reader c_longblob: Array[Integer] + attr_reader c_longtext: String + attr_reader c_json: String + def initialize: ( + Array[Integer] c_bit, + Integer c_tinyint, + Integer c_bool, + Integer c_boolean, + Integer c_smallint, + Integer c_mediumint, + Integer c_int, + Integer c_integer, + Integer c_bigint, + Integer c_serial, + String c_decimal, + String c_dec, + String c_numeric, + String c_fixed, + Float c_float, + Float c_double, + Float c_double_precision, + String c_date, + String c_time, + String c_datetime, + String c_timestamp, + Integer c_year, + String c_char, + String c_nchar, + String c_national_char, + String c_varchar, + Array[Integer] c_binary, + Array[Integer] c_varbinary, + Array[Integer] c_tinyblob, + String c_tinytext, + Array[Integer] c_blob, + String c_text, + Array[Integer] c_mediumblob, + String c_mediumtext, + Array[Integer] c_longblob, + String c_longtext, + String c_json + ) -> void + end + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + class ManyParamsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + end + + class QuerySql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> void + + def update_author: (UpdateAuthorArgs) -> void + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/Gemfile b/test/golden/mysql2_extended/ruby31_gemfile/Gemfile new file mode 100644 index 0000000..e0ff304 --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'connection_pool' +gem 'mysql2' \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb new file mode 100644 index 0000000..1155fae --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,27 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Struct.new(c0) + end + + class My_queriesSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs new file mode 100644 index 0000000..f3f0844 --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,16 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> WithNumbers2GoArgs + end + + class My_queriesSql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/options.json b/test/golden/mysql2_extended/ruby31_gemfile/options.json new file mode 100644 index 0000000..8e3677f --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.1", + "generateGemfile": true +} diff --git a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb b/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb new file mode 100644 index 0000000..5e16504 --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb @@ -0,0 +1,56 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Struct.new(c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs new file mode 100644 index 0000000..a104bdb --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,35 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + NoColsSql: String + + class NoColsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> NoColsArgs + end + + OtherOneSql: String + + class OtherOneRow < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> OtherOneRow + end + + class OtherSql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..cf2dde0 --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb @@ -0,0 +1,294 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Struct.new(name, bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Struct.new(bio, id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Struct.new(id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Struct.new( + c_bit, + c_tinyint, + c_bool, + c_boolean, + c_smallint, + c_mediumint, + c_int, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_dec, + c_numeric, + c_fixed, + c_float, + c_double, + c_double_precision, + c_date, + c_time, + c_datetime, + c_timestamp, + c_year, + c_char, + c_nchar, + c_national_char, + c_varchar, + c_binary, + c_varbinary, + c_tinyblob, + c_tinytext, + c_blob, + c_text, + c_mediumblob, + c_mediumtext, + c_longblob, + c_longtext, + c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + stmt.execute(*query_params) + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + stmt.execute(*query_params) + return client.last_id + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..0603e1b --- /dev/null +++ b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,217 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String + def self.new: ( String name, String bio) -> CreateAuthorArgs + end + + UpdateAuthorSql: String + + class UpdateAuthorArgs < ::Struct + attr_accessor bio: String + attr_accessor id: Integer + def self.new: ( String bio, Integer id) -> UpdateAuthorArgs + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String + def self.new: ( String name, String bio) -> CreateAuthorReturnIdArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + TestSql: String + + class TestRow < ::Struct + attr_accessor c_bit: Array[Integer] + attr_accessor c_tinyint: Integer + attr_accessor c_bool: Integer + attr_accessor c_boolean: Integer + attr_accessor c_smallint: Integer + attr_accessor c_mediumint: Integer + attr_accessor c_int: Integer + attr_accessor c_integer: Integer + attr_accessor c_bigint: Integer + attr_accessor c_serial: Integer + attr_accessor c_decimal: String + attr_accessor c_dec: String + attr_accessor c_numeric: String + attr_accessor c_fixed: String + attr_accessor c_float: Float + attr_accessor c_double: Float + attr_accessor c_double_precision: Float + attr_accessor c_date: String + attr_accessor c_time: String + attr_accessor c_datetime: String + attr_accessor c_timestamp: String + attr_accessor c_year: Integer + attr_accessor c_char: String + attr_accessor c_nchar: String + attr_accessor c_national_char: String + attr_accessor c_varchar: String + attr_accessor c_binary: Array[Integer] + attr_accessor c_varbinary: Array[Integer] + attr_accessor c_tinyblob: Array[Integer] + attr_accessor c_tinytext: String + attr_accessor c_blob: Array[Integer] + attr_accessor c_text: String + attr_accessor c_mediumblob: Array[Integer] + attr_accessor c_mediumtext: String + attr_accessor c_longblob: Array[Integer] + attr_accessor c_longtext: String + attr_accessor c_json: String + def self.new: ( + Array[Integer] c_bit, + Integer c_tinyint, + Integer c_bool, + Integer c_boolean, + Integer c_smallint, + Integer c_mediumint, + Integer c_int, + Integer c_integer, + Integer c_bigint, + Integer c_serial, + String c_decimal, + String c_dec, + String c_numeric, + String c_fixed, + Float c_float, + Float c_double, + Float c_double_precision, + String c_date, + String c_time, + String c_datetime, + String c_timestamp, + Integer c_year, + String c_char, + String c_nchar, + String c_national_char, + String c_varchar, + Array[Integer] c_binary, + Array[Integer] c_varbinary, + Array[Integer] c_tinyblob, + String c_tinytext, + Array[Integer] c_blob, + String c_text, + Array[Integer] c_mediumblob, + String c_mediumtext, + Array[Integer] c_longblob, + String c_longtext, + String c_json + ) -> TestRow + end + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + end + + class QuerySql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> void + + def update_author: (UpdateAuthorArgs) -> void + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..ed1a6a3 --- /dev/null +++ b/test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,27 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/options.json b/test/golden/mysql2_extended/ruby32_notypes/options.json new file mode 100644 index 0000000..ea1ba9b --- /dev/null +++ b/test/golden/mysql2_extended/ruby32_notypes/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.2", + "generateTypes": false +} diff --git a/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb b/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb new file mode 100644 index 0000000..5853c6b --- /dev/null +++ b/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb @@ -0,0 +1,56 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb b/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..d7d0d13 --- /dev/null +++ b/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb @@ -0,0 +1,294 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Data.define(:bio, :id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + stmt.execute(*query_params) + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + stmt.execute(*query_params) + return client.last_id + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/my_queries_sql.rb b/test/golden/pg_extended/default/my_queries_sql.rb new file mode 100644 index 0000000..46a3001 --- /dev/null +++ b/test/golden/pg_extended/default/my_queries_sql.rb @@ -0,0 +1,34 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + unless @prepared_statements.include?('with_numbers2_go') + client.prepare('with_numbers2_go', WithNumbers2GoSql) + @prepared_statements.add('with_numbers2_go') + end + client.exec_prepared('with_numbers2_go', query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/my_queries_sql.rbs b/test/golden/pg_extended/default/my_queries_sql.rbs new file mode 100644 index 0000000..d746da3 --- /dev/null +++ b/test/golden/pg_extended/default/my_queries_sql.rbs @@ -0,0 +1,17 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + + class My_queriesSql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/options.json b/test/golden/pg_extended/default/options.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/test/golden/pg_extended/default/options.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/golden/pg_extended/default/other_sql.rb b/test/golden/pg_extended/default/other_sql.rb new file mode 100644 index 0000000..1681c08 --- /dev/null +++ b/test/golden/pg_extended/default/other_sql.rb @@ -0,0 +1,66 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + NoColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + unless @prepared_statements.include?('no_cols') + client.prepare('no_cols', NoColsSql) + @prepared_statements.add('no_cols') + end + client.exec_prepared('no_cols', query_params) + end + end + + def other_one + @db.with do |client| + unless @prepared_statements.include?('other_one') + client.prepare('other_one', OtherOneSql) + @prepared_statements.add('other_one') + end + result = client.exec_prepared('other_one') + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/other_sql.rbs b/test/golden/pg_extended/default/other_sql.rbs new file mode 100644 index 0000000..73b58f0 --- /dev/null +++ b/test/golden/pg_extended/default/other_sql.rbs @@ -0,0 +1,36 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + NoColsSql: String + + class NoColsArgs + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: Array[Integer] + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> void + end + + OtherOneSql: String + + class OtherOneRow + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + + class OtherSql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/query_sql.rb b/test/golden/pg_extended/default/query_sql.rb new file mode 100644 index 0000000..f9e0f67 --- /dev/null +++ b/test/golden/pg_extended/default/query_sql.rb @@ -0,0 +1,259 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/query_sql.rbs b/test/golden/pg_extended/default/query_sql.rbs new file mode 100644 index 0000000..c0ae90a --- /dev/null +++ b/test/golden/pg_extended/default/query_sql.rbs @@ -0,0 +1,167 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String + def initialize: ( String name, String bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + TestSql: String + + class TestRow + attr_reader c_bit: Array[Integer] + attr_reader c_smallint: Integer + attr_reader c_boolean: bool + attr_reader c_integer: Integer + attr_reader c_bigint: Integer + attr_reader c_serial: Integer + attr_reader c_decimal: Float + attr_reader c_numeric: Float + attr_reader c_real: Float + attr_reader c_double_precision: Float + attr_reader c_date: String + attr_reader c_time: String + attr_reader c_timestamp: String + attr_reader c_char: String + attr_reader c_varchar: String + attr_reader c_bytea: Array[Integer] + attr_reader c_text: String + attr_reader c_json: String + def initialize: ( + Array[Integer] c_bit, + Integer c_smallint, + bool c_boolean, + Integer c_integer, + Integer c_bigint, + Integer c_serial, + Float c_decimal, + Float c_numeric, + Float c_real, + Float c_double_precision, + String c_date, + String c_time, + String c_timestamp, + String c_char, + String c_varchar, + Array[Integer] c_bytea, + String c_text, + String c_json + ) -> void + end + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: bool + attr_reader c1: Integer + def initialize: ( bool c0, Integer c1) -> void + end + + class ManyParamsArgs + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: Array[Integer] + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: Array[Integer] + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + def initialize: ( bool c0, Integer c1, String c2, Float c3) -> void + end + + class QuerySql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> CreateAuthorRow? + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/Gemfile b/test/golden/pg_extended/ruby31_gemfile/Gemfile new file mode 100644 index 0000000..be6f7f1 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'connection_pool' +gem 'pg' +gem 'set' \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb new file mode 100644 index 0000000..bc47f95 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,34 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class WithNumbers2GoArgs < Struct.new(c0) + end + + class My_queriesSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + unless @prepared_statements.include?('with_numbers2_go') + client.prepare('with_numbers2_go', WithNumbers2GoSql) + @prepared_statements.add('with_numbers2_go') + end + client.exec_prepared('with_numbers2_go', query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs new file mode 100644 index 0000000..22b7f69 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,17 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> WithNumbers2GoArgs + end + + class My_queriesSql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/options.json b/test/golden/pg_extended/ruby31_gemfile/options.json new file mode 100644 index 0000000..8e3677f --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.1", + "generateGemfile": true +} diff --git a/test/golden/pg_extended/ruby31_gemfile/other_sql.rb b/test/golden/pg_extended/ruby31_gemfile/other_sql.rb new file mode 100644 index 0000000..5e9f4a8 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/other_sql.rb @@ -0,0 +1,66 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + NoColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class NoColsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class OtherOneRow < Struct.new(c0) + end + + class OtherSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + unless @prepared_statements.include?('no_cols') + client.prepare('no_cols', NoColsSql) + @prepared_statements.add('no_cols') + end + client.exec_prepared('no_cols', query_params) + end + end + + def other_one + @db.with do |client| + unless @prepared_statements.include?('other_one') + client.prepare('other_one', OtherOneSql) + @prepared_statements.add('other_one') + end + result = client.exec_prepared('other_one') + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/other_sql.rbs b/test/golden/pg_extended/ruby31_gemfile/other_sql.rbs new file mode 100644 index 0000000..a4fb600 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,36 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + NoColsSql: String + + class NoColsArgs < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: Array[Integer] + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> NoColsArgs + end + + OtherOneSql: String + + class OtherOneRow < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> OtherOneRow + end + + class OtherSql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/query_sql.rb b/test/golden/pg_extended/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..fbcb595 --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/query_sql.rb @@ -0,0 +1,259 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Struct.new(id, name, bio) + end + + class CreateAuthorArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Struct.new(id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Struct.new( + c_bit, + c_smallint, + c_boolean, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_numeric, + c_real, + c_double_precision, + c_date, + c_time, + c_timestamp, + c_char, + c_varchar, + c_bytea, + c_text, + c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs b/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..65fb25a --- /dev/null +++ b/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,167 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> CreateAuthorRow + end + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String + def self.new: ( String name, String bio) -> CreateAuthorArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + TestSql: String + + class TestRow < ::Struct + attr_accessor c_bit: Array[Integer] + attr_accessor c_smallint: Integer + attr_accessor c_boolean: bool + attr_accessor c_integer: Integer + attr_accessor c_bigint: Integer + attr_accessor c_serial: Integer + attr_accessor c_decimal: Float + attr_accessor c_numeric: Float + attr_accessor c_real: Float + attr_accessor c_double_precision: Float + attr_accessor c_date: String + attr_accessor c_time: String + attr_accessor c_timestamp: String + attr_accessor c_char: String + attr_accessor c_varchar: String + attr_accessor c_bytea: Array[Integer] + attr_accessor c_text: String + attr_accessor c_json: String + def self.new: ( + Array[Integer] c_bit, + Integer c_smallint, + bool c_boolean, + Integer c_integer, + Integer c_bigint, + Integer c_serial, + Float c_decimal, + Float c_numeric, + Float c_real, + Float c_double_precision, + String c_date, + String c_time, + String c_timestamp, + String c_char, + String c_varchar, + Array[Integer] c_bytea, + String c_text, + String c_json + ) -> TestRow + end + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + def self.new: ( bool c0, Integer c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: Array[Integer] + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: Array[Integer] + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + def self.new: ( bool c0, Integer c1, String c2, Float c3) -> FourColsRow + end + + class QuerySql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> CreateAuthorRow? + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..46a3001 --- /dev/null +++ b/test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,34 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def with_numbers2_go(with_numbers2_go_args) + @db.with do |client| + query_params = [with_numbers2_go_args.c0] + unless @prepared_statements.include?('with_numbers2_go') + client.prepare('with_numbers2_go', WithNumbers2GoSql) + @prepared_statements.add('with_numbers2_go') + end + client.exec_prepared('with_numbers2_go', query_params) + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby32_notypes/options.json b/test/golden/pg_extended/ruby32_notypes/options.json new file mode 100644 index 0000000..ea1ba9b --- /dev/null +++ b/test/golden/pg_extended/ruby32_notypes/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.2", + "generateTypes": false +} diff --git a/test/golden/pg_extended/ruby32_notypes/other_sql.rb b/test/golden/pg_extended/ruby32_notypes/other_sql.rb new file mode 100644 index 0000000..1681c08 --- /dev/null +++ b/test/golden/pg_extended/ruby32_notypes/other_sql.rb @@ -0,0 +1,66 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + NoColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + unless @prepared_statements.include?('no_cols') + client.prepare('no_cols', NoColsSql) + @prepared_statements.add('no_cols') + end + client.exec_prepared('no_cols', query_params) + end + end + + def other_one + @db.with do |client| + unless @prepared_statements.include?('other_one') + client.prepare('other_one', OtherOneSql) + @prepared_statements.add('other_one') + end + result = client.exec_prepared('other_one') + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby32_notypes/query_sql.rb b/test/golden/pg_extended/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..f9e0f67 --- /dev/null +++ b/test/golden/pg_extended/ruby32_notypes/query_sql.rb @@ -0,0 +1,259 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + ManyParamsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- $1 $2 + WHERE a = $3) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + client.type_map_for_results = PG::BasicTypeMapForResults.new client + client + } + @prepared_statements = Set[] + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/my_queries_sql.rb b/test/golden/sqlite3_extended/default/my_queries_sql.rb new file mode 100644 index 0000000..5fb9256 --- /dev/null +++ b/test/golden/sqlite3_extended/default/my_queries_sql.rb @@ -0,0 +1,24 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(db) + @db = db + end + + def with_numbers2_go(with_numbers2_go_args) + client = @db + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/my_queries_sql.rbs b/test/golden/sqlite3_extended/default/my_queries_sql.rbs new file mode 100644 index 0000000..05db55c --- /dev/null +++ b/test/golden/sqlite3_extended/default/my_queries_sql.rbs @@ -0,0 +1,16 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + class My_queriesSql + @db: untyped + def initialize: (untyped) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/options.json b/test/golden/sqlite3_extended/default/options.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/test/golden/sqlite3_extended/default/options.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/golden/sqlite3_extended/default/other_sql.rb b/test/golden/sqlite3_extended/default/other_sql.rb new file mode 100644 index 0000000..dcf3ef3 --- /dev/null +++ b/test/golden/sqlite3_extended/default/other_sql.rb @@ -0,0 +1,52 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(db) + @db = db + end + + def no_cols(no_cols_args) + client = @db + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + + def other_one + client = @db + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/other_sql.rbs b/test/golden/sqlite3_extended/default/other_sql.rbs new file mode 100644 index 0000000..c3f60c5 --- /dev/null +++ b/test/golden/sqlite3_extended/default/other_sql.rbs @@ -0,0 +1,35 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + NoColsSql: String + + class NoColsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: bool + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> void + end + + OtherOneSql: String + + class OtherOneRow + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + class OtherSql + @db: untyped + def initialize: (untyped) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/query_sql.rb b/test/golden/sqlite3_extended/default/query_sql.rb new file mode 100644 index 0000000..5061b6d --- /dev/null +++ b/test/golden/sqlite3_extended/default/query_sql.rb @@ -0,0 +1,159 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/query_sql.rbs b/test/golden/sqlite3_extended/default/query_sql.rbs new file mode 100644 index 0000000..27784f0 --- /dev/null +++ b/test/golden/sqlite3_extended/default/query_sql.rbs @@ -0,0 +1,114 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String + def initialize: ( Integer id, String name, String bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String + def initialize: ( String name, String bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + class ManyParamsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: bool + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: bool + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + end + + class QuerySql + @db: untyped + def initialize: (untyped) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> void + + def delete_author: (DeleteAuthorArgs) -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/Gemfile b/test/golden/sqlite3_extended/ruby31_gemfile/Gemfile new file mode 100644 index 0000000..ea36ae7 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'connection_pool' +gem 'sqlite3' \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb new file mode 100644 index 0000000..9a8e0e7 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,24 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Struct.new(c0) + end + + class My_queriesSql + def initialize(db) + @db = db + end + + def with_numbers2_go(with_numbers2_go_args) + client = @db + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs new file mode 100644 index 0000000..fed39ec --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,16 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + WithNumbers2GoSql: String + + class WithNumbers2GoArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> WithNumbers2GoArgs + end + + class My_queriesSql + @db: untyped + def initialize: (untyped) -> void + + def with_numbers2_go: (WithNumbers2GoArgs) -> void + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/options.json b/test/golden/sqlite3_extended/ruby31_gemfile/options.json new file mode 100644 index 0000000..8e3677f --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.1", + "generateGemfile": true +} diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb b/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb new file mode 100644 index 0000000..23a4a2f --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb @@ -0,0 +1,52 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Struct.new(c0) + end + + class OtherSql + def initialize(db) + @db = db + end + + def no_cols(no_cols_args) + client = @db + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + + def other_one + client = @db + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs new file mode 100644 index 0000000..6b68028 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,35 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + NoColsSql: String + + class NoColsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: bool + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> NoColsArgs + end + + OtherOneSql: String + + class OtherOneRow < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> OtherOneRow + end + + class OtherSql + @db: untyped + def initialize: (untyped) -> void + + def no_cols: (NoColsArgs) -> void + + def other_one: -> OtherOneRow? + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..af36cb9 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb @@ -0,0 +1,159 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Struct.new(id) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..08b7cda --- /dev/null +++ b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,114 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String + def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String + def self.new: ( String name, String bio) -> CreateAuthorArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: bool + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: bool + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + end + + class QuerySql + @db: untyped + def initialize: (untyped) -> void + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> void + + def create_author: (CreateAuthorArgs) -> void + + def delete_author: (DeleteAuthorArgs) -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> void + + def four_cols: -> void + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..5fb9256 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,24 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class WithNumbers2GoArgs < Data.define(:c0) + end + + class My_queriesSql + def initialize(db) + @db = db + end + + def with_numbers2_go(with_numbers2_go_args) + client = @db + query_params = [with_numbers2_go_args.c0] + stmt = client.prepare(WithNumbers2GoSql) + stmt.execute(*query_params) + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/options.json b/test/golden/sqlite3_extended/ruby32_notypes/options.json new file mode 100644 index 0000000..ea1ba9b --- /dev/null +++ b/test/golden/sqlite3_extended/ruby32_notypes/options.json @@ -0,0 +1,4 @@ +{ + "rubyVersion": "3.2", + "generateTypes": false +} diff --git a/test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb b/test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb new file mode 100644 index 0000000..dcf3ef3 --- /dev/null +++ b/test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb @@ -0,0 +1,52 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + class OtherSql + def initialize(db) + @db = db + end + + def no_cols(no_cols_args) + client = @db + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + stmt.execute(*query_params) + end + + def other_one + client = @db + stmt = client.prepare(OtherOneSql) + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb b/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..5061b6d --- /dev/null +++ b/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb @@ -0,0 +1,159 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + stmt.execute(*query_params) + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + stmt.execute(*query_params) + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + end + end +end \ No newline at end of file diff --git a/test/golden_test.rb b/test/golden_test.rb new file mode 100644 index 0000000..81213ca --- /dev/null +++ b/test/golden_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +# Diffs the generated files against the C# plugin's output checked in under examples/. +class GoldenTest < Minitest::Test + include PluginHelper + + { + 'pg' => 'pg', + 'mysql2' => 'mysql2', + 'sqlite3' => 'sqlite3' + }.each do |driver, example| + %w[query_sql.rb query_sql.rbs].each do |name| + define_method("test_#{driver}_#{name.tr('.', '_')}_matches_csharp_output") do + files = generate(load_request(driver)) + expected = File.read(File.join(ROOT, 'examples', example, name)) + assert_equal normalize(expected), normalize(files.fetch(name)) + end + end + end +end diff --git a/ruby-wasm-poc/test/fixture.rb b/test/official_protobuf.rb similarity index 93% rename from ruby-wasm-poc/test/fixture.rb rename to test/official_protobuf.rb index a77bd5e..46732d5 100644 --- a/ruby-wasm-poc/test/fixture.rb +++ b/test/official_protobuf.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -# Builds a GenerateRequest equivalent to what sqlc would send for -# examples/authors/postgresql (hand-built, since sqlc isn't runnable here). -# Uses the official google-protobuf gem as an independent encoder so the -# plugin's pure-Ruby decoder is verified against a real protobuf implementation. +# Builds a GenerateRequest by hand (pg) with the official google-protobuf gem as an independent encoder, +# so the plugin's pure-Ruby codec is cross-checked against a real protobuf implementation. require 'google/protobuf' module Fixture diff --git a/test/options_test.rb b/test/options_test.rb new file mode 100644 index 0000000..a83a33e --- /dev/null +++ b/test/options_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require_relative 'test_helper' +require 'sqlc_gen_ruby/options' + +class OptionsTest < Minitest::Test + Options = SqlcGenRuby::Options + RubyVersion = SqlcGenRuby::RubyVersion + + def opts(hash) = Options.new(JSON.generate(hash)) + + def test_defaults + o = opts(driver: 'pg') + assert_equal 'pg', o.driver_name + assert_equal false, o.generate_gemfile + assert_equal true, o.generate_types + assert_equal '3.3', o.ruby_version.to_s + end + + def test_driver_is_case_insensitive_and_validated + assert_equal 'mysql2', opts(driver: 'Mysql2').driver_name + assert_raises(ArgumentError) { opts(driver: 'oracle') } + assert_raises(ArgumentError) { opts({}) } + end + + def test_version_bounds + %w[3.1 3.2 3.3 3].each { |v| RubyVersion.parse(v) } + err = assert_raises(ArgumentError) { RubyVersion.parse('3.4') } + assert_equal 'Provided version 3.4 exceeds max version 3.3', err.message + err = assert_raises(ArgumentError) { RubyVersion.parse('3.0') } + assert_equal 'Provided version 3.0 exceeds min version 3.1', err.message + assert_raises(ArgumentError) { RubyVersion.parse('2.7') } + assert_raises(ArgumentError) { RubyVersion.parse('three') } + assert_raises(ArgumentError) { RubyVersion.parse('3.03') } + end + + # The C# comparison treats a patch component as greater than a missing one. + def test_patch_versions_compare_like_the_csharp_implementation + assert_raises(ArgumentError) { RubyVersion.parse('3.3.1') } + RubyVersion.parse('3.2.9') + end + + def test_immutable_data_support + refute RubyVersion.parse('3.1').immutable_data_supported? + assert RubyVersion.parse('3.2').immutable_data_supported? + assert RubyVersion.parse('3.3').immutable_data_supported? + assert RubyVersion.parse(' 3.2 ').immutable_data_supported? + end + + def test_option_types_are_validated + assert_raises(ArgumentError) { opts(driver: 'pg', generateTypes: 'yes') } + assert_raises(ArgumentError) { opts(driver: 'pg', rubyVersion: 3.3) } + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..bac54e9 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'open3' +require 'json' +require 'minitest/autorun' +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'sqlc_gen_ruby/plugin_pb' + +# Helpers to run the real plugin entry point as a subprocess over stdin/stdout, exactly like sqlc does. +module PluginHelper + ROOT = File.expand_path('..', __dir__) + PLUGIN = File.join(ROOT, 'exe/plugin.rb') + FIXTURES = File.join(ROOT, 'test/fixtures') + Plugin = SqlcGenRuby::Plugin + + # Real GenerateRequest bytes captured from `sqlc generate` (see scripts/capture_request.rb). + def load_request(driver, **options) + request = Plugin::GenerateRequest.decode(File.binread(File.join(FIXTURES, "#{driver}.bin"))) + unless options.empty? + merged = JSON.parse(request.plugin_options).merge(options.transform_keys(&:to_s)) + request.plugin_options = JSON.generate(merged) + end + request + end + + def run_plugin(request_bytes) + out, err, status = Open3.capture3(RbConfig.ruby, PLUGIN, stdin_data: request_bytes, binmode: true) + [out, err, status] + end + + # Returns { 'name' => contents } or raises with the plugin's stderr. + def generate(request) + out, err, status = run_plugin(request.encode) + raise "plugin failed: #{err}" unless status.success? + + Plugin::GenerateResponse.decode(out).files.to_h { |f| [f.name, f.contents.dup.force_encoding('UTF-8')] } + end + + # The C# plugin's checked-in output contains a few CRs from CRLF line endings in its source. + def normalize(text) = text.delete("\r").strip +end From 0d8b20d576c1b856ae1c0c58307d409d28d4be71 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:34:41 +0000 Subject: [PATCH 05/15] Make the Ruby WASM plugin the primary one: Rakefile, sqlc configs, CI, docs - Rakefile: `test`, `wasm_build`, `sqlc_generate_{process,wasm}`, `test_*_plugin` - sqlc.local.yaml runs exe/plugin.rb (PATH passed through: sqlc starts plugins with an empty environment); sqlc.ci.yaml keeps using dist/plugin.wasm - CI: unit/golden tests + WASM build with Ruby (no .NET), sqlc 1.27.0 - build: strip the 18 MB of DWARF/name sections from the prebuilt interpreter and trim more of the embedded stdlib: dist/plugin.wasm 37 MB -> 18 MB - Dockerfile: skip build/test-only gem groups - regenerate examples/ with the Ruby plugin (only difference vs the C# output is the stray CRs the C# raw string literals put into the header lines) - README, docs/Design.md Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .github/workflows/ci.yml | 64 +++++++---------------- .github/workflows/docs.yml | 2 +- Dockerfile | 2 + README.md | 53 +++++++++++-------- Rakefile | 53 +++++++++++-------- docs/Design.md | 96 +++++++++++++++++----------------- examples/mysql2/query_sql.rb | 6 +-- examples/mysql2/query_sql.rbs | 2 +- examples/pg/query_sql.rb | 6 +-- examples/pg/query_sql.rbs | 2 +- examples/sqlite3/query_sql.rb | 6 +-- examples/sqlite3/query_sql.rbs | 2 +- scripts/wasm/build.sh | 17 ++++-- scripts/wasm/strip_wasm.rb | 48 +++++++++++++++++ sqlc.local.yaml | 4 +- 15 files changed, 207 insertions(+), 156 deletions(-) create mode 100644 scripts/wasm/strip_wasm.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 037deb4..b3150dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,73 +13,47 @@ on: paths: - "**" - "!**.md" - - "!ruby-wasm-poc/**" pull_request: branches: [ "*" ] paths: - "**" - "!**.md" - - "!ruby-wasm-poc/**" jobs: - lint: - name: Lint + test: + name: Plugin Tests runs-on: ubuntu-latest steps: - - name: Check out Git repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up .NET - uses: actions/setup-dotnet@v4 + - uses: ruby/setup-ruby@v1 with: - dotnet-version: "8.0.x" + ruby-version: '3.3' + bundler-cache: true - - name: .NET Lint - uses: zyactions/dotnet-lint@v1 - with: - implicit-restore: true - workspace: sqlc-gen-ruby.sln - exclude: GeneratedProtobuf + # unit tests + golden tests against the output of the original C# implementation + - name: Unit & golden tests + run: bundle exec rake test build: name: Build - runs-on: ${{ matrix.os }} - needs: [lint] - strategy: - matrix: - os: [ 'ubuntu-latest' ] - architecture: [ 'x64', 'arm64' ] + runs-on: ubuntu-latest + needs: [test] steps: - uses: actions/checkout@v4 - - name: Load .env file - uses: xom9ikk/dotenv@v2.3.0 - with: - load-mode: strict - - name: Setup Dotnet - uses: actions/setup-dotnet@v4 + - uses: ruby/setup-ruby@v1 with: - dotnet-version: '8.0.x' - - - name: Install Wasi workload - run: dotnet workload install wasi-experimental - - - name: Download Wasi SDK on Ubuntu - if: runner.os == 'Linux' - run: | - wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz - tar xf wasi-sdk-22.0-linux.tar.gz - chmod -R 755 wasi-sdk-22.0 + ruby-version: '3.3' + bundler-cache: true - - name: Dotnet Wasm Publish - run: | - WASI_SDK_PATH="$(pwd)/wasi-sdk-22.0" dotnet publish WasmRunner -c release - ./scripts/wasm/copy_plugin_to.sh dist + # pure Ruby plugin + prebuilt ruby.wasm interpreter, packed into one self-contained module + - name: Build WASM plugin + run: ./scripts/wasm/build.sh - name: upload wasm plugin as artifact - if: matrix.architecture == 'arm64' # this needs to happen once across matrix uses: actions/upload-artifact@v4 with: name: wasm-file @@ -118,7 +92,7 @@ jobs: - uses: sqlc-dev/setup-sqlc@v4 with: - sqlc-version: '1.25.0' + sqlc-version: '1.27.0' - name: Updating plugin sha run: ./scripts/wasm/update_sha.sh ${SQLC_CI_FILE} @@ -145,7 +119,7 @@ jobs: - uses: sqlc-dev/setup-sqlc@v4 with: - sqlc-version: '1.25.0' + sqlc-version: '1.27.0' - name: Load .env file uses: xom9ikk/dotenv@v2.3.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ed5b4ee..0c525cd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ on: - "sqlc.ci.yaml" - "examples/**" - "tests/**" - - "Drivers/**" + - "lib/**" jobs: generate-and-push-docs: diff --git a/Dockerfile b/Dockerfile index a73205e..0806115 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ FROM ruby:3.2 # throw errors if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 +# the end2end image only needs the gems used by the generated code, not the wasm build / codec test tooling +RUN bundle config --global without 'wasm_build codec_test' WORKDIR /usr/src/app diff --git a/README.md b/README.md index 6493586..97a447a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ [![CI](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/ci.yml) -This project is up for grabs, in anyone is interested in developing it further. I decided to focus on the C# plugin. - # sqlc-gen-ruby -Why I didn't use Ruby to generate Ruby in [here](ruby-wasm-poc/README.md). +A [sqlc](https://sqlc.dev) plugin that generates Ruby code (plus optional RBS type signatures) from SQL. +Supported drivers: `pg`, `mysql2` and `sqlite3`. + +The plugin itself is written in Ruby: it is pure Ruby with no native dependencies (it has its own tiny protobuf +codec), and it is shipped as a single self-contained WebAssembly module — the Ruby interpreter +([ruby.wasm](https://github.com/ruby/ruby.wasm)) with the plugin embedded. See [docs/Design.md](docs/Design.md). ## Usage @@ -64,33 +67,36 @@ sql: ### [Generated Code Examples](docs/Examples.md) -### Next features -1. Supporting sqlite engine, utilizing sqlite3 gem -2. Adding type support to generated code by generating .rbs files - # Contributing ## Local plugin development ### Prerequisites -make sure that the following applications are installed and exposed in your path - -Follow the instructions in each of these: -* Ruby - [Ruby Installation](https://www.ruby-lang.org/en/downloads/) -* Dotnet CLI - [Dotnet Installation](https://github.com/dotnet/sdk) - use version `.NET 8.0 (latest)` -* buf build - [Buf Build](https://buf.build/docs/installation) -* WASM related - [WASM libs](https://www.strathweb.com/2023/09/dotnet-wasi-applications-in-net-8-0/) - -### Protobuf -SQLC protobuf are defined in sqlc-dev/sqlc repository. -Generating Ruby code from protocol buffer files: +* Ruby 3.3 and `bundle install` +* [sqlc](https://docs.sqlc.dev/en/latest/overview/install.html) (v1.27.0 was used for verification) +* `curl` + `tar` for building the WASM plugin (it downloads the prebuilt ruby.wasm interpreter) +* Docker + docker-compose, for the end-to-end tests only (they need real MySQL / PostgreSQL servers) + +### Layout +| Path | What | +|---|---| +| `exe/plugin.rb` | plugin entry point: `GenerateRequest` on stdin → `GenerateResponse` on stdout | +| `lib/sqlc_gen_ruby/` | the generator (`generator.rb`, `method_gen.rb`, `drivers/*.rb`, `options.rb`, ...) | +| `test/` | unit + golden tests (`rake test`); `test/fixtures` are real requests captured from sqlc | +| `tests/` | end-to-end tests of the generated code against real databases | +| `scripts/wasm/` | building the WASM plugin | + +### Unit & golden tests ``` -rake protobuf_generate +bundle exec rake test ``` +The golden tests feed the plugin real `GenerateRequest`s captured from sqlc and diff the result against +[examples/](examples) (and against `test/golden`, recorded from the original C# implementation). +To refresh a fixture see `scripts/capture_request.rb`. ### Generating code SQLC utilizes our process / WASM plugin to generate code ``` -rake sqlc_generate_process -rake sqlc_generate_wasm +rake sqlc_generate_process # runs exe/plugin.rb directly (needs `ruby` on PATH) +rake sqlc_generate_wasm # builds dist/plugin.wasm first and runs that ``` ### Testing generated code @@ -100,6 +106,11 @@ rake test_process_plugin rake test_wasm_plugin ``` +### Building the WASM plugin +``` +rake wasm_build # == ./scripts/wasm/build.sh, writes dist/plugin.wasm (~18 MB) +``` + ## Release flow The release flow in this repo follows the semver conventions, building tag as `v[major].[minor].[patch]`. diff --git a/Rakefile b/Rakefile index 96db2b9..d583c95 100755 --- a/Rakefile +++ b/Rakefile @@ -2,51 +2,58 @@ require 'rake' require 'rake/testtask' +# Plugin unit + golden tests (pure Ruby, no databases needed). +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.pattern = 'test/*_test.rb' +end + Rake::TestTask.new(:end2end_tests) do |t| # called inside Docker - t.pattern = "tests/end2end_*.rb" + t.pattern = 'tests/end2end_*.rb' end task :steep do - sh "bundle exec steep check --with-expectations" + sh 'bundle exec steep check --with-expectations' end task :run_end2end_tests do - sh "./scripts/tests/run_end2end.sh" + sh './scripts/tests/run_end2end.sh' end task :run_tests => [:run_end2end_tests] -# Other -task :dotnet_format do - sh "dotnet format --exclude GeneratedProtobuf" +# --- Process plugin (exe/plugin.rb run directly by sqlc) --- +task :sqlc_generate_process do + sh 'sqlc -f sqlc.local.yaml generate' end -task :protobuf_generate do - sh "./scripts/generate_protobuf.sh" +task :test_process_plugin => [:test, :sqlc_generate_process, :run_tests] + +# --- WASM plugin (dist/plugin.wasm: ruby.wasm interpreter + this plugin, see scripts/wasm/build.sh) --- +task :wasm_build do + sh './scripts/wasm/build.sh' end -task :dotnet_publish_process => [:protobuf_generate, :dotnet_format] do - sh "dotnet publish LocalRunner -c release --output dist/" +task :update_wasm_plugin do + sh './scripts/wasm/update_sha.sh sqlc.ci.yaml' end -task :sqlc_generate_process => [:dotnet_publish_process] do - sh "sqlc -f sqlc.local.yaml generate" +task :sqlc_generate_wasm => [:wasm_build, :update_wasm_plugin] do + ENV['SQLCCACHE'] = './' + sh 'sqlc -f sqlc.ci.yaml generate' end -task :test_process_plugin => [:sqlc_generate_process, :run_tests] +task :test_wasm_plugin => [:test, :sqlc_generate_wasm, :update_wasm_plugin, :run_tests] -task :dotnet_publish_wasm => :protobuf_generate do - sh "dotnet publish WasmRunner -c release --output dist/" - sh "./scripts/wasm/copy_plugin_to.sh dist" +# --- Legacy C# implementation (removed once the Ruby port is the only plugin) --- +task :protobuf_generate do + sh './scripts/generate_protobuf.sh' end -task :update_wasm_plugin do - sh "./scripts/wasm/update_sha.sh sqlc.ci.yaml" +task :dotnet_format do + sh 'dotnet format --exclude GeneratedProtobuf' end -task :sqlc_generate_wasm => [:dotnet_publish_wasm, :update_wasm_plugin] do - ENV['SQLCCACHE'] = './' - sh "sqlc -f sqlc.ci.yaml generate" +task :dotnet_publish_process => [:protobuf_generate, :dotnet_format] do + sh 'dotnet publish LocalRunner -c release --output dist/' end - -task :test_wasm_plugin => [:sqlc_generate_wasm, :update_wasm_plugin, :run_tests] diff --git a/docs/Design.md b/docs/Design.md index 91f138d..46f49b3 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -1,53 +1,51 @@ -# sqlc-gen-ruby in Ruby (POC) +# Design: a sqlc plugin written in Ruby, shipped as WASM -Goal: write the sqlc plugin in Ruby instead of C#, and ship it as a `.wasm`. +sqlc plugins receive a protobuf `GenerateRequest` on stdin and answer with a `GenerateResponse` on stdout, either as a +process or as a WASI module. This plugin is pure Ruby and is distributed as a WASI module (`plugin.wasm`). -## What works (verified) +## Source layout -The plugin logic is now **pure Ruby with zero native dependencies**: - -| File | Replaces | +| File | Role | |---|---| -| `lib/sqlc_gen_ruby/proto.rb` | `google-protobuf` (a C extension, which cannot run inside ruby.wasm). A ~150 line wire-format codec. | -| `lib/sqlc_gen_ruby/plugin_pb.rb` | The generated `GenerateRequest/Query/Column/...` classes (subset of `codegen.proto`). | -| `lib/sqlc_gen_ruby/generator.rb`, `drivers/pg.rb`, `options.rb`, `code.rb` | `CodeGenerator.cs`, `PgDriver.cs`, `MethodGen.cs`, `RubySyntax/*`, `PluginOptions/*`. | -| `exe/plugin.rb` | `PluginRunner.cs`: stdin = `GenerateRequest`, stdout = `GenerateResponse`. | - -`test/golden_test.rb` runs `exe/plugin.rb` as a subprocess over stdin/stdout (the way sqlc does) -with a request built by the official `google-protobuf` gem, and asserts the generated -`query_sql.rb` / `query_sql.rbs` equal what the C# plugin produced (`examples/pg`), ignoring CRs -and the trailing newline. - -```bash -cd ruby-wasm-poc && bundle install && bundle exec ruby test/golden_test.rb -``` - -Scope of the port: only the `pg` driver and the `:one` / `:many` / `:exec` commands. `mysql2`, -`sqlite3` and `:execlastid` still need porting (the structure in `drivers/pg.rb` is the template). - -## The WASM plugin (verified end to end) - -`scripts/build_wasm.sh` produces `dist/plugin.wasm` (~37 MB, one self-contained file): - -1. downloads the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release (this sidesteps the old - `rbwasm build` `dln.c` / `Dl_info` failure entirely); -2. stages a slim filesystem (core stdlib minus bundler/rdoc/irb/etc. + `lib/` + `exe/`); -3. embeds it into the interpreter with `rbwasm pack` (wasi-vfs), since sqlc gives plugins no filesystem. - -**The argv problem is solved by a trick:** sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, -so the interpreter treats that path as its script. The build embeds a file at exactly that path containing -`load '/app/exe/plugin.rb'`. - -Verified with sqlc v1.27.0 (`wasm: url: file://...dist/plugin.wasm`) on `examples/authors/postgresql`: -`sqlc generate` takes ~2s and the generated `query_sql.rb` / `.rbs` are identical to the C# plugin's -(`examples/pg`), ignoring CRs. Also checked under wasmtime v27: WASM output is byte-identical to native Ruby. - -```bash -gem install ruby_wasm && ruby-wasm-poc/scripts/build_wasm.sh # set RBWASM="ruby /path/to/rbwasm" if rbwasm isn't on PATH -``` - -## Remaining work -- Port the `mysql2` and `sqlite3` drivers and `:execlastid` (use `drivers/pg.rb` as the template). -- Port the remaining C# options/edge cases (e.g. `generateGemfile` path is written but untested). -- Shrink the wasm (37 MB, mostly the interpreter) and wire it into CI (`sqlc.ci.yaml`). -- Decide on removing the C# projects once the Ruby port has parity. +| `exe/plugin.rb` | entry point (stdin → `Generator` → stdout) | +| `lib/sqlc_gen_ruby/proto.rb` | ~150 line protobuf wire codec. `google-protobuf` is a C extension and cannot be loaded inside ruby.wasm, so the plugin brings its own | +| `lib/sqlc_gen_ruby/plugin_pb.rb` | the subset of sqlc's `codegen.proto` the generator needs (field numbers must match upstream) | +| `lib/sqlc_gen_ruby/options.rb`, `ruby_version.rb` | the `options:` JSON, `rubyVersion` parsing / validation (3.1 – 3.3; `Struct` below 3.2, `Data` from 3.2) | +| `lib/sqlc_gen_ruby/generator.rb` | module / class / dataclass (`Data.define`, `Struct.new`) layout, `.rb` / `.rbs` / `Gemfile` files | +| `lib/sqlc_gen_ruby/method_gen.rb` | one generated method per query: `:one`, `:many`, `:exec`, `:execlastid` | +| `lib/sqlc_gen_ruby/drivers/*.rb` | `Base` + `Pg`, `Mysql2`, `Sqlite3`: column type mapping, statement preparation / execution, pooling, row access | +| `lib/sqlc_gen_ruby/code.rb` | indent / snake_case helpers (formatting must stay byte-identical with the original implementation) | + +Driver differences are data, not copy/paste: `Base` has `pooling_enabled?` (ConnectionPool vs a single injected +connection), `row_data_type` (`row['col']` vs `row[0]`), `supports_exec_last_id?` (mysql2 only) and a column mapping +table (exact or prefix based matching). + +## The WASM build (`scripts/wasm/build.sh`) + +1. downloads the *prebuilt* `ruby-3.3-wasm32-unknown-wasip1-full` release from ruby/ruby.wasm (this avoids compiling + CRuby with a wasi-sdk); +2. strips the ~18 MB of DWARF debug info / name section from the interpreter (`strip_wasm.rb`); +3. stages a slim stdlib (the plugin only uses core + `json`) + `lib/` + `exe/`; +4. embeds that filesystem into the interpreter with `rbwasm pack` (wasi-vfs), because sqlc gives plugins no filesystem. + +**The argv trick:** sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, so the interpreter treats that +path as its script. The build embeds a file at exactly that path containing `load '/app/exe/plugin.rb'`. + +## Verification strategy + +The previous C# implementation defined the expected behaviour. Its output is kept as test data: + +* `examples/{pg,mysql2,sqlite3}` — checked-in output of the C# plugin for the example schemas; +* `test/golden/*` — C# output for option variants (Ruby 3.1 `Struct`, 3.2 / no types, `generateGemfile`) over the sqlc + captured queries plus synthetic ones (5+ params / columns for the multi-line formatting, lower-case names, several + query files); +* `test/fixtures/*.bin` — real `GenerateRequest`s captured from `sqlc generate` (v1.27.0). + +`test/codec_test.rb` cross-checks the hand-written protobuf codec against Google's official implementation (test-only +dependency). + +## Known quirks kept for output compatibility +* With Ruby < 3.2 the generated `Struct.new(id, name)` lists bare identifiers, not symbols (`:id, :name`). + This is what the C# plugin generated, so it is preserved. +* `3.3.x` (a patch version) is rejected: the original version comparison treats a patch as greater than "no patch". +* An unknown `driver` is an error here (the C# enum parse silently fell back to `mysql2`). diff --git a/examples/mysql2/query_sql.rb b/examples/mysql2/query_sql.rb index aaf9357..f3d6626 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -1,7 +1,7 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit require 'connection_pool' -require 'mysql2' - +require 'mysql2' + module Mysql2Codegen GetAuthorSql = %q(SELECT id, name, bio FROM authors WHERE id = ? LIMIT 1) diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index 83d9f88..1bf0841 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -1,4 +1,4 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit module Mysql2Codegen GetAuthorSql: String diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index d8c7630..dab0710 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -1,8 +1,8 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' - +require 'set' + module PgCodegen GetAuthorSql = %q(SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1) diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index 87aefaf..c0ce377 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -1,4 +1,4 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit module PgCodegen GetAuthorSql: String diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index 6bca88f..c7f128c 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -1,7 +1,7 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit require 'connection_pool' -require 'sqlite3' - +require 'sqlite3' + module Sqlite3Codegen GetAuthorSql = %q(SELECT id, name, bio FROM authors WHERE id = ? LIMIT 1) diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index 5b09491..466fdfd 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -1,4 +1,4 @@ -# auto-generated by sqlc - do not edit +# auto-generated by sqlc - do not edit module Sqlite3Codegen GetAuthorSql: String diff --git a/scripts/wasm/build.sh b/scripts/wasm/build.sh index df4d392..08419c1 100755 --- a/scripts/wasm/build.sh +++ b/scripts/wasm/build.sh @@ -3,11 +3,12 @@ # Needs: ruby + `gem install ruby_wasm` (for `rbwasm pack`), curl, tar. set -euo pipefail cd "$(dirname "$0")/../.." - - RUBY_WASM_VERSION="${RUBY_WASM_VERSION:-2.7.1}" FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" -RBWASM="${RBWASM:-rbwasm}" +# `rbwasm` comes from the ruby_wasm gem (see Gemfile, group :wasm_build) +if [ -z "${RBWASM:-}" ]; then + if command -v rbwasm >/dev/null 2>&1; then RBWASM="rbwasm"; else RBWASM="bundle exec rbwasm"; fi +fi WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT @@ -16,12 +17,20 @@ curl -fsSL -o "$WORK/ruby.tgz" \ "https://github.com/ruby/ruby.wasm/releases/download/${RUBY_WASM_VERSION}/${FLAVOR}.tar.gz" tar -xzf "$WORK/ruby.tgz" -C "$WORK" SRC="$WORK/$FLAVOR/usr/local" +# the prebuilt interpreter carries ~18 MB of DWARF debug info + names: strip it +ruby scripts/wasm/strip_wasm.rb "$SRC/bin/ruby" # 2. stage a slim filesystem to embed: core stdlib only + the plugin. sqlc gives plugins no filesystem. STAGE="$WORK/stage" mkdir -p "$STAGE/usr/local/lib/ruby" "$STAGE/app" cp -r "$SRC/lib/ruby/3.3.0" "$STAGE/usr/local/lib/ruby/3.3.0" -(cd "$STAGE/usr/local/lib/ruby/3.3.0" && rm -rf bundler rdoc irb reline net ruby_vm rbs typeprof debug) +# Not needed by the plugin (it only uses core + json). Anything required at interpreter boot (rubygems, +# monitor, ...) must stay; test/wasm_smoke_test.sh / `sqlc generate` catch an over-trimmed stdlib. +(cd "$STAGE/usr/local/lib/ruby/3.3.0" && rm -rf bundler rdoc irb reline net ruby_vm rbs typeprof debug \ + prism syntax_suggest csv csv.rb drb drb.rb cgi cgi.rb openssl openssl.rb mkmf.rb resolv.rb resolv-replace.rb \ + psych psych.rb yaml yaml.rb unicode_normalize optparse optparse.rb open3 open3.rb ripper ripper.rb \ + random rinda benchmark benchmark.rb pstore.rb tempfile.rb tmpdir.rb un.rb webrick expect.rb getoptlong.rb \ + observer.rb abbrev.rb base64.rb mutex_m.rb nkf.rb syslog.rb) mkdir -p "$STAGE/usr/local/lib/ruby/site_ruby" "$STAGE/usr/local/lib/ruby/vendor_ruby" "$STAGE/usr/local/lib/ruby/gems" cp -r lib exe "$STAGE/app/" # sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, so the interpreter treats that diff --git a/scripts/wasm/strip_wasm.rb b/scripts/wasm/strip_wasm.rb new file mode 100644 index 0000000..21706b1 --- /dev/null +++ b/scripts/wasm/strip_wasm.rb @@ -0,0 +1,48 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Removes debug info (DWARF `.debug_*` custom sections) and the `name` section from a WebAssembly module. +# They are ~18 MB of the 34 MB prebuilt ruby.wasm and are not needed at runtime. +# Dependency-free so the build only needs Ruby (no wabt / binaryen). +# +# ruby strip_wasm.rb in.wasm [out.wasm] (default: in place) +DROP = ->(name) { name == 'name' || name.start_with?('.debug') } + +def read_leb(data, pos) + result = 0 + shift = 0 + loop do + byte = data.getbyte(pos) + pos += 1 + result |= (byte & 0x7f) << shift + shift += 7 + return [result, pos] if byte < 0x80 + end +end + +input = ARGV.fetch(0) +output = ARGV[1] || input +data = File.binread(input) +abort "#{input}: not a wasm module" unless data.byteslice(0, 4) == "\0asm".b + +out = data.byteslice(0, 8) +pos = 8 +dropped = 0 +while pos < data.bytesize + id = data.getbyte(pos) + size, payload = read_leb(data, pos + 1) + section_end = payload + size + drop = false + if id.zero? + name_len, name_start = read_leb(data, payload) + drop = DROP.call(data.byteslice(name_start, name_len)) + end + if drop + dropped += section_end - pos + else + out << data.byteslice(pos, section_end - pos) + end + pos = section_end +end +File.binwrite(output, out) +warn "strip_wasm: #{data.bytesize} -> #{out.bytesize} bytes (removed #{dropped})" diff --git a/sqlc.local.yaml b/sqlc.local.yaml index 2da54e9..13a6df0 100644 --- a/sqlc.local.yaml +++ b/sqlc.local.yaml @@ -2,7 +2,9 @@ version: "2" plugins: - name: ruby process: - cmd: ./dist/LocalRunner + cmd: ./exe/plugin.rb + env: + - PATH # sqlc runs plugins with an empty environment; exe/plugin.rb needs `ruby` on PATH sql: - schema: "examples/authors/postgresql/schema.sql" queries: "examples/authors/postgresql/query.sql" From c542b14a1bcce613c45c6e131815476928a0f041 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 12:37:32 +0000 Subject: [PATCH 06/15] REMOVE C# implementation (revertible: `git revert` this commit to restore it) The Ruby plugin was verified output-identical to the C# plugin before this commit (see docs/Design.md "Verification strategy"): - 432 option/query combinations diffed against the C# binary, 0 mismatches - real `sqlc generate` through dist/plugin.wasm == checked-in examples - `sqlc diff` clean Removes the C# projects (CodeGenerator, Drivers, Extensions, GeneratedProtobuf, LocalRunner, PluginOptions, RubySyntax, SqlcGenRuby, WasmRunner), the .sln, buf.gen.yaml, the C# .editorconfig, the dotnet/protobuf/wasm-copy scripts, the legacy Rakefile tasks and the SOURCE_WASM* entries in .env. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .editorconfig | 201 - .env | 2 - .github/workflows/ci.yml | 2 +- CodeGenerator/CodeGenerator.cs | 205 - CodeGenerator/CodeGenerator.csproj | 19 - Drivers/ClassMember.cs | 24 - Drivers/ColumnMappingConfig.cs | 9 - Drivers/DbDriver.cs | 58 - Drivers/Drivers.csproj | 15 - Drivers/MethodGen.cs | 144 - Drivers/Mysql2Driver.cs | 122 - Drivers/PgDriver.cs | 153 - Drivers/RowDataType.cs | 7 - Drivers/Sqlite3Driver.cs | 110 - Drivers/Variable.cs | 27 - Extensions/Extensions.csproj | 15 - Extensions/ListExtensions.cs | 28 - Extensions/StringExtensions.cs | 58 - GeneratedProtobuf/Codegen.cs | 5197 -------------------- GeneratedProtobuf/GeneratedProtobuf.csproj | 14 - LocalRunner/App.cs | 9 - LocalRunner/LocalRunner.csproj | 15 - LocalRunner/Properties/AssemblyInfo.cs | 6 - LocalRunner/nuget.config | 11 - PluginOptions/Options.cs | 37 - PluginOptions/PluginOptions.csproj | 18 - PluginOptions/RawOptions.cs | 18 - PluginOptions/RubyVersion.cs | 69 - Rakefile | 13 - RubySyntax/Basic.cs | 167 - RubySyntax/Flows.cs | 94 - RubySyntax/Interfaces.cs | 13 - RubySyntax/RubySyntax.csproj | 13 - SqlcGenRuby/PluginRunner.cs | 30 - SqlcGenRuby/SqlcGenRuby.csproj | 18 - WasmRunner/App.cs | 3 - WasmRunner/Properties/AssemblyInfo.cs | 6 - WasmRunner/WasmRunner.csproj | 21 - WasmRunner/runtimeconfig.template.json | 10 - buf.gen.yaml | 4 - scripts/generate_dockerfile.sh | 23 - scripts/generate_protobuf.sh | 7 - scripts/wasm/copy_plugin_to.sh | 15 - sqlc-gen-ruby.sln | 73 - sqlc-gen-ruby.sln.DotSettings | 2 - 45 files changed, 1 insertion(+), 7104 deletions(-) delete mode 100644 .editorconfig delete mode 100644 CodeGenerator/CodeGenerator.cs delete mode 100644 CodeGenerator/CodeGenerator.csproj delete mode 100644 Drivers/ClassMember.cs delete mode 100644 Drivers/ColumnMappingConfig.cs delete mode 100644 Drivers/DbDriver.cs delete mode 100644 Drivers/Drivers.csproj delete mode 100644 Drivers/MethodGen.cs delete mode 100644 Drivers/Mysql2Driver.cs delete mode 100644 Drivers/PgDriver.cs delete mode 100644 Drivers/RowDataType.cs delete mode 100644 Drivers/Sqlite3Driver.cs delete mode 100644 Drivers/Variable.cs delete mode 100644 Extensions/Extensions.csproj delete mode 100644 Extensions/ListExtensions.cs delete mode 100644 Extensions/StringExtensions.cs delete mode 100644 GeneratedProtobuf/Codegen.cs delete mode 100644 GeneratedProtobuf/GeneratedProtobuf.csproj delete mode 100644 LocalRunner/App.cs delete mode 100644 LocalRunner/LocalRunner.csproj delete mode 100644 LocalRunner/Properties/AssemblyInfo.cs delete mode 100644 LocalRunner/nuget.config delete mode 100644 PluginOptions/Options.cs delete mode 100644 PluginOptions/PluginOptions.csproj delete mode 100644 PluginOptions/RawOptions.cs delete mode 100644 PluginOptions/RubyVersion.cs delete mode 100644 RubySyntax/Basic.cs delete mode 100644 RubySyntax/Flows.cs delete mode 100644 RubySyntax/Interfaces.cs delete mode 100644 RubySyntax/RubySyntax.csproj delete mode 100644 SqlcGenRuby/PluginRunner.cs delete mode 100644 SqlcGenRuby/SqlcGenRuby.csproj delete mode 100644 WasmRunner/App.cs delete mode 100644 WasmRunner/Properties/AssemblyInfo.cs delete mode 100644 WasmRunner/WasmRunner.csproj delete mode 100644 WasmRunner/runtimeconfig.template.json delete mode 100644 buf.gen.yaml delete mode 100755 scripts/generate_dockerfile.sh delete mode 100755 scripts/generate_protobuf.sh delete mode 100755 scripts/wasm/copy_plugin_to.sh delete mode 100644 sqlc-gen-ruby.sln delete mode 100644 sqlc-gen-ruby.sln.DotSettings diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b78fe7e..0000000 --- a/.editorconfig +++ /dev/null @@ -1,201 +0,0 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories -root = true - -# C# files -[*.cs] - -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_size = 4 -indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = crlf -insert_final_newline = false - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false -file_header_template = unset - -# this. and Me. preferences -dotnet_style_qualification_for_event = false -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false -dotnet_style_qualification_for_property = false - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# Expression-level preferences -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:silent - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = true -csharp_style_var_for_built_in_types = true -csharp_style_var_when_type_is_apparent = true - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = false -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = false -csharp_style_expression_bodied_properties = true - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true - -# Null-checking preferences -csharp_style_conditional_delegate_call = true - -# Modifier preferences -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async - -# Code-block preferences -csharp_prefer_braces = true -csharp_prefer_simple_using_statement = true - -# Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_inlined_variable_declaration = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace - -#### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file diff --git a/.env b/.env index 877516e..2404df3 100644 --- a/.env +++ b/.env @@ -1,5 +1,3 @@ -SOURCE_WASM_FILE_UBUNTU="WasmRunner/obj/release/net8.0/wasi-wasm/wasm/for-publish/WasmRunner.wasm" -SOURCE_WASM_FILE="WasmRunner/bin/Release/net8.0/wasi-wasm/AppBundle/WasmRunner.wasm" TESTS_CONTAINER_NAME="plugin-tests" DB_NAME="tests" DB_USER="root" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3150dc..a09d6b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: ruby-version: '3.3' bundler-cache: true - # unit tests + golden tests against the output of the original C# implementation + # unit tests + golden tests - name: Unit & golden tests run: bundle exec rake test diff --git a/CodeGenerator/CodeGenerator.cs b/CodeGenerator/CodeGenerator.cs deleted file mode 100644 index 2e126a8..0000000 --- a/CodeGenerator/CodeGenerator.cs +++ /dev/null @@ -1,205 +0,0 @@ -using Google.Protobuf; -using Plugin; -using RubyCodegen; -using SqlcGenRuby.Drivers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using File = Plugin.File; - -namespace SqlcGenRuby; - -public class CodeGenerator -{ - private const string AutoGeneratedComment = "# auto-generated by sqlc - do not edit"; - - private DbDriver? _dbDriver; - private Options? _options; - - private Options Options - { - get => _options!; - set => _options = value; - } - - private DbDriver DbDriver - { - get => _dbDriver!; - set => _dbDriver = value; - } - - private DbDriver InstantiateDriver() - { - return Options.DriverName switch - { - DriverName.Mysql2 => new Mysql2Driver(), - DriverName.Pg => new PgDriver(), - DriverName.Sqlite3 => new Sqlite3Driver(), - _ => throw new ArgumentException($"unknown driver: {Options.DriverName}") - }; - } - - public Task Generate(GenerateRequest generateRequest) - { - Options = new Options(generateRequest); - DbDriver = InstantiateDriver(); - var generatedFiles = GetFileQueries() - .SelectMany(fq => GenerateFiles(fq.Value, fq.Key)) - .AppendIf(GenerateGemfile(), Options.GenerateGemfile); - return Task.FromResult(new GenerateResponse { Files = { generatedFiles } }); - - Dictionary GetFileQueries() - { - return generateRequest.Queries - .GroupBy(query => QueryFilenameToClassName(query.Filename)) - .ToDictionary( - group => group.Key, - group => group.ToArray()); - } - - string QueryFilenameToClassName(string filenameWithExtension) - { - return string.Concat( - Path.GetFileNameWithoutExtension(filenameWithExtension).FirstCharToUpper(), - Path.GetExtension(filenameWithExtension)[1..].FirstCharToUpper()); - } - } - - private IEnumerable GenerateFiles(IList queries, string className) - { - var (requiredGems, moduleDeclaration) = GenerateModule(queries, className); - return new List() - .Append(GenerateCodeFile(className, requiredGems, moduleDeclaration)) - .AppendIf(GenerateTypedefFile(className, moduleDeclaration), Options.GenerateTypes); - } - - private static File GenerateCodeFile(string className, IEnumerable requiredGems, - ModuleDeclaration moduleDeclaration) - { - return new File - { - Name = $"{className.SnakeCase()}.rb", - Contents = ByteString.CopyFromUtf8( - $""" - {AutoGeneratedComment} - {requiredGems.Select(r => r.Build()).JoinByNewLine()} - - {moduleDeclaration.BuildCode()} - """ - ) - }; - } - - private static File GenerateTypedefFile(string className, ModuleDeclaration moduleDeclaration) - { - return new File - { - Name = $"{className.SnakeCase()}.rbs", - Contents = ByteString.CopyFromUtf8( - $""" - {AutoGeneratedComment} - {moduleDeclaration.BuildType()} - """ - ) - }; - } - - private File GenerateGemfile() - { - return new File - { - Name = "Gemfile", - Contents = ByteString.CopyFromUtf8( - $""" - source 'https://rubygems.org' - - {DbDriver.GetRequiredGems().Select(gem => $"gem '{gem.Name()}'").JoinByNewLine()} - """ - ) - }; - } - - private (IList, ModuleDeclaration) GenerateModule(IList queries, - string className) - { - var requiredGems = DbDriver.GetRequiredGems(); - var initMethod = DbDriver.GetInitMethod(); - var classDeclaration = GetClassDeclaration(); - return (requiredGems, GetModuleDeclaration()); - - ModuleDeclaration GetModuleDeclaration() - { - return new ModuleDeclaration($"{Options.DriverName.ToString()}Codegen", - queries - .SelectMany(q => new List() - .Append(DbDriver.QueryTextConstantDeclare(q)) - .AppendIfNotNull(GetQueryColumnsDataclass(q)) - .AppendIfNotNull(GetQueryParamsDataclass(q))) - .Append(classDeclaration)); - } - - ClassDeclaration GetClassDeclaration() - { - return new ClassDeclaration( - className, - new[] { initMethod }.Concat(queries.Select(GetMethodDeclaration)) - ); - } - } - - private IComposableRbsType GenerateDataclass(string funcName, ClassMember classMember, IList columns, - Options options) - { - var dataclassName = $"{funcName.FirstCharToUpper()}{classMember.Name()}"; - var nameToType = columns.ToDictionary( - kv => kv.Name, - kv => DbDriver.GetColumnType(kv) - ); - return options.RubyVersion.ImmutableDataSupported() - ? new DataDefine(dataclassName, nameToType) - : new NewStruct(dataclassName, nameToType); - } - - private IComposableRbsType? GetQueryColumnsDataclass(Query query) - { - return query.Columns.Count <= 0 - ? null - : GenerateDataclass(query.Name, ClassMember.Row, query.Columns, Options); - } - - - private IComposableRbsType? GetQueryParamsDataclass(Query query) - { - if (query.Params.Count <= 0) - return null; - var columns = query.Params.Select(p => p.Column).ToList(); - return GenerateDataclass(query.Name, ClassMember.Args, columns, Options); - } - - private MethodDeclaration GetMethodDeclaration(Query query) - { - var queryTextConstant = GetInterfaceName(ClassMember.Sql); - var argInterface = GetInterfaceName(ClassMember.Args); - var returnInterface = GetInterfaceName(ClassMember.Row); - var funcName = query.Name.SnakeCase(); - - return query.Cmd switch - { - ":one" => DbDriver.OneDeclare(funcName, queryTextConstant, argInterface, returnInterface, - query.Params, query.Columns), - ":many" => DbDriver.ManyDeclare(funcName, queryTextConstant, argInterface, returnInterface, - query.Params, query.Columns), - ":exec" => DbDriver.ExecDeclare(funcName, queryTextConstant, argInterface, query.Params), - ":execlastid" => ((Mysql2Driver)DbDriver) - .ExecLastIdDeclare(funcName, queryTextConstant, argInterface, query.Params), - _ => throw new InvalidDataException() - }; - - string GetInterfaceName(ClassMember classMemberType) - { - return $"{query.Name}{classMemberType.Name()}"; - } - } -} \ No newline at end of file diff --git a/CodeGenerator/CodeGenerator.csproj b/CodeGenerator/CodeGenerator.csproj deleted file mode 100644 index e16be1d..0000000 --- a/CodeGenerator/CodeGenerator.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - SqlcGenRuby - - - - - - - - - - - - - diff --git a/Drivers/ClassMember.cs b/Drivers/ClassMember.cs deleted file mode 100644 index 132176c..0000000 --- a/Drivers/ClassMember.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace SqlcGenRuby.Drivers; - -public enum ClassMember -{ - Row, - Args, - Sql -} - -public static class ClassMemberTypeExtensions -{ - public static string Name(this ClassMember me) - { - return me switch - { - ClassMember.Sql => "Sql", - ClassMember.Row => "Row", - ClassMember.Args => "Args", - _ => throw new ArgumentOutOfRangeException(nameof(me), me, null) - }; - } -} \ No newline at end of file diff --git a/Drivers/ColumnMappingConfig.cs b/Drivers/ColumnMappingConfig.cs deleted file mode 100644 index f7378cb..0000000 --- a/Drivers/ColumnMappingConfig.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace SqlcGenRuby.Drivers; - -public readonly record struct ColumnMappingConfig( - string rubyType, - HashSet dbTypes, - bool isPrefixBased -); \ No newline at end of file diff --git a/Drivers/DbDriver.cs b/Drivers/DbDriver.cs deleted file mode 100644 index 5aef4d9..0000000 --- a/Drivers/DbDriver.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Plugin; -using RubyCodegen; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SqlcGenRuby.Drivers; - -public abstract class DbDriver -{ - protected static IList GetCommonGems() - { - return [new RequireGem("connection_pool")]; - } - - public abstract IList GetRequiredGems(); - - public abstract MethodDeclaration GetInitMethod(); - - protected abstract IEnumerable GetColumnMapping(); - - public string GetColumnType(Column column) - { - var dbType = column.Type.Name.ToLower(); // from SQLC - char(5), char(10), int4, int8, float8, blob, etc. - foreach (var columnMapping in GetColumnMapping()) - { - var rubyType = columnMapping.isPrefixBased ? GetByPrefix(columnMapping) : GetByValue(columnMapping); - if (rubyType is not null) - return rubyType; - } - throw new NotSupportedException($"Unsupported column type: {column.Type.Name}"); - - string? GetByValue(ColumnMappingConfig columnMapping) - { - return columnMapping.dbTypes.Contains(dbType) ? columnMapping.rubyType : null; - } - - string? GetByPrefix(ColumnMappingConfig columnMapping) - { - return columnMapping.dbTypes.Any(d => d.StartsWith(dbType)) ? columnMapping.rubyType : null; - } - } - - public abstract PropertyDeclaration QueryTextConstantDeclare(Query query); - - public abstract IComposable PrepareStmt(string funcName, string queryTextConstant); - - public abstract SimpleExpression ExecuteStmt(string funcName, SimpleStatement? queryParams); - - public abstract MethodDeclaration OneDeclare(string name, string sqlTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns); - - public abstract MethodDeclaration ManyDeclare(string funcName, string sqlTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns); - - public abstract MethodDeclaration ExecDeclare(string funcName, string text, string argInterface, - IList parameters); -} \ No newline at end of file diff --git a/Drivers/Drivers.csproj b/Drivers/Drivers.csproj deleted file mode 100644 index fc42378..0000000 --- a/Drivers/Drivers.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net8.0 - enable - SqlcGenRuby.Drivers - - - - - - - - - diff --git a/Drivers/MethodGen.cs b/Drivers/MethodGen.cs deleted file mode 100644 index a52dd93..0000000 --- a/Drivers/MethodGen.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Plugin; -using RubyCodegen; -using System.Collections.Generic; -using System.Linq; - -namespace SqlcGenRuby.Drivers; - -public class MethodGen(DbDriver dbDriver) -{ - private static string? GetMethodArgs(string argInterface, IList parameters) - { - return parameters.Count == 0 ? null : argInterface.SnakeCase(); - } - - public MethodDeclaration OneDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns, bool poolingEnabled = true, - RowDataType rowDataType = RowDataType.Hash) - { - var newObjectExpression = new NewObject(returnInterface, GetColumnsInitExpressions(columns, rowDataType)); - IEnumerable withResourceBody = new List(); - var queryParams = GetQueryParams(argInterface, parameters); - withResourceBody = withResourceBody.AppendIfNotNull(queryParams); - withResourceBody = withResourceBody - .Concat( - [ - dbDriver.PrepareStmt(funcName, queryTextConstant), - ExecuteAndAssign(funcName, queryParams), - new SimpleStatement(Variable.Row.AsVar(), new SimpleExpression($"{Variable.Result.AsVar()}.first")), - new SimpleExpression($"return nil if {Variable.Row.AsVar()}.nil?"), - new SimpleStatement($"{Variable.Entity.AsVar()}", newObjectExpression), - new SimpleExpression($"return {Variable.Entity.AsVar()}") - ] - ).ToList(); - - var methodArgs = GetMethodArgs(argInterface, parameters); - var methodBody = OptionallyAddPoolUsage(poolingEnabled, withResourceBody); - return new MethodDeclaration(funcName, argInterface, methodArgs, $"{returnInterface}?", methodBody); - } - - private static IEnumerable OptionallyAddPoolUsage(bool poolingEnabled, IEnumerable body) - { - return poolingEnabled - ? - [ - new WithResource( - Variable.Db.AsProperty(), - Variable.Client.AsVar(), - body.ToList()) - ] - : new List - { - new SimpleExpression($"{Variable.Client.AsVar()} = {Variable.Db.AsProperty()}") - } - .Concat(body); - } - - public MethodDeclaration ManyDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns, bool poolingEnabled = true, - RowDataType rowDataType = RowDataType.Hash) - { - var queryParams = GetQueryParams(argInterface, parameters); - var withResourceBody = new List() - .AppendIfNotNull(queryParams) - .Append(dbDriver.PrepareStmt(funcName, queryTextConstant)) - .Append(ExecuteAndAssign(funcName, queryParams)) - .Append(new SimpleStatement(Variable.Entities.AsVar(), new SimpleExpression("[]"))) - .Append(AssignResultInForeach()) - .Append(new SimpleExpression($"return {Variable.Entities.AsVar()}")); - - var methodArgs = GetMethodArgs(argInterface, parameters); - var methodBody = OptionallyAddPoolUsage(poolingEnabled, withResourceBody); - return new MethodDeclaration(funcName, argInterface, methodArgs, null, methodBody); - - ForeachLoop AssignResultInForeach() - { - var listAppend = new ListAppend(Variable.Entities.AsVar(), - new NewObject(returnInterface, GetColumnsInitExpressions(columns, rowDataType))); - return new ForeachLoop( - Variable.Result.AsVar(), - Variable.Row.AsVar(), - new List { listAppend }); - } - } - - public MethodDeclaration ExecDeclare(string funcName, string queryTextConstant, string argInterface, - IList parameters, bool poolingEnabled = true) - { - var queryParams = GetQueryParams(argInterface, parameters); - var withResourceBody = new List() - .AppendIfNotNull(queryParams) - .Append(dbDriver.PrepareStmt(funcName, queryTextConstant)) - .Append(dbDriver.ExecuteStmt(funcName, queryParams)) - .ToList(); - - var methodArgs = GetMethodArgs(argInterface, parameters); - var methodBody = OptionallyAddPoolUsage(poolingEnabled, withResourceBody); - return new MethodDeclaration(funcName, argInterface, methodArgs, null, methodBody); - } - - public MethodDeclaration ExecLastIdDeclare(string funcName, string queryTextConstant, string argInterface, - IList parameters) - { - var queryParams = GetQueryParams(argInterface, parameters); - var withResourceBody = new List() - .AppendIfNotNull(queryParams) - .Append(dbDriver.PrepareStmt(funcName, queryTextConstant)) - .Append(dbDriver.ExecuteStmt(funcName, queryParams)) - .Append(new SimpleExpression($"return {Variable.Client.AsVar()}.last_id")); - - return new MethodDeclaration( - funcName, - argInterface, - GetMethodArgs(argInterface, parameters), - "Integer", - new List - { - new WithResource(Variable.Db.AsProperty(), Variable.Client.AsVar(), - withResourceBody.ToList()) - } - ); - } - - private static SimpleStatement? GetQueryParams(string argInterface, IList parameters) - { - var queryParams = parameters.Select(p => $"{argInterface.SnakeCase()}.{p.Column.Name}").ToList(); - return queryParams.Count == 0 - ? null - : new SimpleStatement(Variable.QueryParams.AsVar(), - new SimpleExpression($"[{queryParams.JoinByCommaAndFormat()}]")); - } - - private static IList GetColumnsInitExpressions(IList columns, RowDataType rowDataType) - { - return rowDataType == RowDataType.Hash - ? columns.Select(c => new SimpleExpression($"{Variable.Row.AsVar()}['{c.Name}']")).ToList() - : columns.Select((_, i) => new SimpleExpression($"{Variable.Row.AsVar()}[{i}]")).ToList(); - } - - private SimpleStatement ExecuteAndAssign(string funcName, SimpleStatement? queryParams) - { - return new SimpleStatement(Variable.Result.AsVar(), - dbDriver.ExecuteStmt(funcName, queryParams)); - } -} \ No newline at end of file diff --git a/Drivers/Mysql2Driver.cs b/Drivers/Mysql2Driver.cs deleted file mode 100644 index da2ae7c..0000000 --- a/Drivers/Mysql2Driver.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Plugin; -using RubyCodegen; -using System.Collections.Generic; -using System.Linq; - -namespace SqlcGenRuby.Drivers; - -public class Mysql2Driver : DbDriver -{ - private MethodGen MethodGen { get; } - - public Mysql2Driver() - { - MethodGen = new MethodGen(this); - } - - public override IList GetRequiredGems() - { - return GetCommonGems().Append(new RequireGem("mysql2")).ToList(); - } - - public override MethodDeclaration GetInitMethod() - { - var connectionPoolInit = new NewObject("ConnectionPool", - new[] { new SimpleExpression("**connection_pool_params") }, - new List { new SimpleExpression("Mysql2::Client.new(**mysql2_params)") }); - return new MethodDeclaration( - "initialize", - "Hash[String, String], Hash[String, String]", - "connection_pool_params, mysql2_params", - null, - [ - new PropertyDeclaration(Variable.Db.AsProperty(), "untyped", connectionPoolInit) - ] - ); - } - - protected override IEnumerable GetColumnMapping() - { - return - [ - new ColumnMappingConfig("Array[Integer]", [ - "binary", - "bit", - "blob", - "longblob", - "mediumblob", - "tinyblob", - "varbinary" - ], false), - new ColumnMappingConfig("String", [ - "char", - "date", - "datetime", - "decimal", - "longtext", - "mediumtext", - "text", - "time", - "timestamp", - "tinytext", - "varchar", - "json" - ], false), - new ColumnMappingConfig("Integer", [ - "bigint", - "int", - "mediumint", - "smallint", - "tinyint", - "year" - ], false), - new ColumnMappingConfig("Float", ["double", "float"], false), - ]; - } - - public override PropertyDeclaration QueryTextConstantDeclare(Query query) - { - return new PropertyDeclaration( - $"{query.Name}{ClassMember.Sql}", - "String", - new SimpleExpression($"%q({query.Text})")); - } - - public override IComposable PrepareStmt(string _, string queryTextConstant) - { - return new SimpleStatement(Variable.Stmt.AsVar(), - new SimpleExpression($"{Variable.Client.AsVar()}.prepare({queryTextConstant})")); - } - - public override SimpleExpression ExecuteStmt(string funcName, SimpleStatement? queryParams) - { - var command = $"{Variable.Stmt.AsVar()}.execute"; - if (queryParams is not null) - command = $"{command}(*{Variable.QueryParams.AsVar()})"; - return new SimpleExpression(command); - } - - public override MethodDeclaration OneDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.OneDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns); - } - - public override MethodDeclaration ExecDeclare(string funcName, string queryTextConstant, string argInterface, - IList parameters) - { - return MethodGen.ExecDeclare(funcName, queryTextConstant, argInterface, parameters); - } - - public MethodDeclaration ExecLastIdDeclare(string funcName, string queryTextConstant, - string argInterface, IList parameters) - { - return MethodGen.ExecLastIdDeclare(funcName, queryTextConstant, argInterface, parameters); - } - - public override MethodDeclaration ManyDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.ManyDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns); - } -} \ No newline at end of file diff --git a/Drivers/PgDriver.cs b/Drivers/PgDriver.cs deleted file mode 100644 index 125f467..0000000 --- a/Drivers/PgDriver.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Plugin; -using RubyCodegen; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace SqlcGenRuby.Drivers; - -public partial class PgDriver : DbDriver -{ - private MethodGen MethodGen { get; } - - public PgDriver() - { - MethodGen = new MethodGen(this); - } - - public override IList GetRequiredGems() - { - return GetCommonGems() - .Append(new RequireGem("pg")) - .Append(new RequireGem("set")) - .ToList(); - } - - public override MethodDeclaration GetInitMethod() - { - var connectionPoolInit = new NewObject("ConnectionPool", - new[] { new SimpleExpression("**connection_pool_params") }, - PgClientCreate()); - return new MethodDeclaration( - "initialize", - "Hash[String, String], Hash[String, String]", - "connection_pool_params, pg_params", - null, - [ - new PropertyDeclaration(Variable.Db.AsProperty(), "untyped", connectionPoolInit), - new PropertyDeclaration(Variable.PreparedStatements.AsProperty(), "Set[String]", new SimpleExpression("Set[]")) - ] - ); - - IList PgClientCreate() - { - return new List - { - new SimpleStatement( - Variable.Client.AsVar(), - new SimpleExpression("PG.connect(**pg_params)")), - new SimpleStatement( - $"{Variable.Client.AsVar()}.type_map_for_results", - new SimpleExpression($"PG::BasicTypeMapForResults.new {Variable.Client.AsVar()}")), - new SimpleExpression(Variable.Client.AsVar()) - }; - } - } - - protected override IEnumerable GetColumnMapping() - { - return - [ - new ColumnMappingConfig("bool", [ - "bool", - "boolean" - ], false), - new ColumnMappingConfig("Array[Integer]", [ - "binary", - "bit", - "bytea", - "blob", - "longblob", - "mediumblob", - "tinyblob", - "varbinary" - ], false), - new ColumnMappingConfig("String", [ - "char", - "date", - "datetime", - "longtext", - "mediumtext", - "text", - "bpchar", - "time", - "timestamp", - "tinytext", - "varchar", - "json" - ], false), - new ColumnMappingConfig("Integer", [ - "int2", - "int4", - "int8", - "serial", - "bigserial" - ], false), - new ColumnMappingConfig("Float", [ - "numeric", - "float4", - "float8", - "decimal" - ], false) - ]; - } - - public override PropertyDeclaration QueryTextConstantDeclare(Query query) - { - var counter = 1; - var transformedQueryText = BindRegexToReplace().Replace(query.Text, _ => $"${counter++}"); - return new PropertyDeclaration( - $"{query.Name}{ClassMember.Sql}", - "String", - new SimpleExpression($"%q({transformedQueryText})")); - } - - public override IComposable PrepareStmt(string funcName, string queryTextConstant) - { - return new UnlessCondition( - $"{Variable.PreparedStatements.AsProperty()}.include?('{funcName}')", - new List - { - new SimpleExpression($"{Variable.Client.AsVar()}.prepare('{funcName}', {queryTextConstant})"), - new SimpleExpression($"{Variable.PreparedStatements.AsProperty()}.add('{funcName}')") - } - ); - } - - public override SimpleExpression ExecuteStmt(string funcName, SimpleStatement? queryParams) - { - var queryParamsArg = queryParams is null ? string.Empty : $", {Variable.QueryParams.AsVar()}"; - return new SimpleExpression($"{Variable.Client.AsVar()}.exec_prepared('{funcName}'{queryParamsArg})"); - } - - public override MethodDeclaration OneDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.OneDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns); - } - - public override MethodDeclaration ExecDeclare(string funcName, string queryTextConstant, string argInterface, - IList parameters) - { - return MethodGen.ExecDeclare(funcName, queryTextConstant, argInterface, parameters); - } - - public override MethodDeclaration ManyDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.ManyDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns); - } - - [GeneratedRegex(@"\?")] - private static partial Regex BindRegexToReplace(); -} \ No newline at end of file diff --git a/Drivers/RowDataType.cs b/Drivers/RowDataType.cs deleted file mode 100644 index 793dbd6..0000000 --- a/Drivers/RowDataType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SqlcGenRuby.Drivers; - -public enum RowDataType -{ - Hash, - Array -} \ No newline at end of file diff --git a/Drivers/Sqlite3Driver.cs b/Drivers/Sqlite3Driver.cs deleted file mode 100644 index 958aadb..0000000 --- a/Drivers/Sqlite3Driver.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Plugin; -using RubyCodegen; -using System.Collections.Generic; -using System.Linq; - -namespace SqlcGenRuby.Drivers; - -public class Sqlite3Driver : DbDriver -{ - private MethodGen MethodGen { get; } - - public Sqlite3Driver() - { - MethodGen = new MethodGen(this); - } - - public override IList GetRequiredGems() - { - return GetCommonGems().Append(new RequireGem("sqlite3")).ToList(); - } - - public override MethodDeclaration GetInitMethod() - { - return new MethodDeclaration("initialize", "untyped", "db", null, - [ - new PropertyDeclaration(Variable.Db.AsProperty(), "untyped", new SimpleExpression("db")) - ] - ); - } - - protected override IEnumerable GetColumnMapping() - { - return - [ - new ColumnMappingConfig("Integer", [ - "int", - "integer", - "tinyint", - "smallint", - "mediumint", - "bigint", - "unsignedbigint", - "int2", - "int8" - ], false), - new ColumnMappingConfig("bool", ["boolean", "bool"], false), - new ColumnMappingConfig("Array[Integer]", ["blob"], false), - new ColumnMappingConfig("String", [ - "character", - "varchar", - "varyingcharacter", - "nchar", - "nativecharacter", - "nvarchar", - "text", - "clob" - ], true), - new ColumnMappingConfig("Float", [ - "real", - "double", - "doubleprecision", - "float", - "decimal", - "numeric" - ], true), - ]; - } - - public override PropertyDeclaration QueryTextConstantDeclare(Query query) - { - return new PropertyDeclaration( - $"{query.Name}{ClassMember.Sql}", - "String", - new SimpleExpression($"%q({query.Text})")); - } - - public override IComposable PrepareStmt(string _, string queryTextConstant) - { - return new SimpleStatement(Variable.Stmt.AsVar(), - new SimpleExpression($"{Variable.Client.AsVar()}.prepare({queryTextConstant})")); - } - - public override SimpleExpression ExecuteStmt(string funcName, SimpleStatement? queryParams) - { - var command = $"{Variable.Stmt.AsVar()}.execute"; - if (queryParams is not null) - command = $"{command}(*{Variable.QueryParams.AsVar()})"; - return new SimpleExpression(command); - } - - public override MethodDeclaration OneDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.OneDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns, - false, RowDataType.Array); - } - - public override MethodDeclaration ExecDeclare(string funcName, string queryTextConstant, string argInterface, - IList parameters) - { - return MethodGen.ExecDeclare(funcName, queryTextConstant, argInterface, parameters, false); - } - - public override MethodDeclaration ManyDeclare(string funcName, string queryTextConstant, string argInterface, - string returnInterface, IList parameters, IList columns) - { - return MethodGen.ManyDeclare(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns, - false, RowDataType.Array); - } -} \ No newline at end of file diff --git a/Drivers/Variable.cs b/Drivers/Variable.cs deleted file mode 100644 index 0a6e9eb..0000000 --- a/Drivers/Variable.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace SqlcGenRuby.Drivers; - -public enum Variable -{ - Db, - PreparedStatements, - QueryParams, - Client, - Stmt, - Result, - Row, - Entity, - Entities -} - -public static class VariablesExtensions -{ - public static string AsVar(this Variable me) - { - return me.ToString().SnakeCase(); - } - - public static string AsProperty(this Variable me) - { - return $"@{me.ToString().SnakeCase()}"; - } -} \ No newline at end of file diff --git a/Extensions/Extensions.csproj b/Extensions/Extensions.csproj deleted file mode 100644 index 52fa923..0000000 --- a/Extensions/Extensions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net8.0 - enable - enable - SqlcGenRuby - - - - - - - - diff --git a/Extensions/ListExtensions.cs b/Extensions/ListExtensions.cs deleted file mode 100644 index bf8c691..0000000 --- a/Extensions/ListExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace SqlcGenRuby; - -public static class ListExtensions -{ - private const int MaxElementsPerLine = 5; - - public static IEnumerable AppendIf(this IEnumerable me, T item, bool condition) - { - return condition ? me.Append(item) : me; - } - - public static IEnumerable AppendIfNotNull(this IEnumerable me, T? item) - { - return item is not null ? me.Append(item) : me; - } - - public static string JoinByNewLine(this IEnumerable me, int cnt = 1) - { - return string.Join(new string('\n', cnt), me); - } - - public static string JoinByCommaAndFormat(this IList me) - { - return me.Count < MaxElementsPerLine - ? string.Join(", ", me) - : $"\n{string.Join(",\n", me).Indent()}\n"; - } -} \ No newline at end of file diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs deleted file mode 100644 index 547ca4a..0000000 --- a/Extensions/StringExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text.RegularExpressions; - -namespace SqlcGenRuby; - -public static partial class StringExtensions -{ - public static string FirstCharToUpper(this string input) - { - return input switch - { - null => throw new ArgumentNullException(nameof(input)), - "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), - _ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1)) - }; - } - - public static string SnakeCase(this string input) - { - return string.IsNullOrEmpty(input) - ? input - : SnakeCaseRegex().Replace(input, "_$1").TrimStart('_').ToLower(); - } - - public static string TrimTrailingWhitespacesPerLine(this string lines) - { - var lengthToRemove = lines - .Split("\n") - .First() - .FirstNonWhitespaceIndex(); - return lines - .Split("\n") - .Select(l => l[lengthToRemove..]) - .JoinByNewLine(); - } - - public static string Indent(this string lines) - { - return lines - .Split("\n") - .Select(line => $"\t{line}") - .JoinByNewLine(); - } - - private static int FirstNonWhitespaceIndex(this string input) - { - if (string.IsNullOrEmpty(input)) - { - return -1; - } - - var trimmed = input.TrimStart(); - return trimmed.Length == 0 ? -1 : input.Length - trimmed.Length; - } - - - [GeneratedRegex("([A-Z])")] - private static partial Regex SnakeCaseRegex(); -} \ No newline at end of file diff --git a/GeneratedProtobuf/Codegen.cs b/GeneratedProtobuf/Codegen.cs deleted file mode 100644 index 5ff800d..0000000 --- a/GeneratedProtobuf/Codegen.cs +++ /dev/null @@ -1,5197 +0,0 @@ -// -// Generated by the protocol buffer compiler. DO NOT EDIT! -// source: plugin/codegen.proto -// -#pragma warning disable 1591, 0612, 3021, 8981 -#region Designer generated code - -using pb = global::Google.Protobuf; -using pbc = global::Google.Protobuf.Collections; -using pbr = global::Google.Protobuf.Reflection; -using scg = global::System.Collections.Generic; -namespace Plugin { - - /// Holder for reflection information generated from plugin/codegen.proto - public static partial class CodegenReflection { - - #region Descriptor - /// File descriptor for plugin/codegen.proto - public static pbr::FileDescriptor Descriptor { - get { return descriptor; } - } - private static pbr::FileDescriptor descriptor; - - static CodegenReflection() { - byte[] descriptorData = global::System.Convert.FromBase64String( - string.Concat( - "ChRwbHVnaW4vY29kZWdlbi5wcm90bxIGcGx1Z2luIjYKBEZpbGUSEgoEbmFt", - "ZRgBIAEoCVIEbmFtZRIaCghjb250ZW50cxgCIAEoDFIIY29udGVudHMitwEK", - "CFNldHRpbmdzEhgKB3ZlcnNpb24YASABKAlSB3ZlcnNpb24SFgoGZW5naW5l", - "GAIgASgJUgZlbmdpbmUSFgoGc2NoZW1hGAMgAygJUgZzY2hlbWESGAoHcXVl", - "cmllcxgEIAMoCVIHcXVlcmllcxIpCgdjb2RlZ2VuGAwgASgLMg8ucGx1Z2lu", - "LkNvZGVnZW5SB2NvZGVnZW5KBAgFEAZKBAgIEAlKBAgJEApKBAgKEAtKBAgL", - "EAwiiwIKB0NvZGVnZW4SEAoDb3V0GAEgASgJUgNvdXQSFgoGcGx1Z2luGAIg", - "ASgJUgZwbHVnaW4SGAoHb3B0aW9ucxgDIAEoDFIHb3B0aW9ucxIQCgNlbnYY", - "BCADKAlSA2VudhIxCgdwcm9jZXNzGAUgASgLMhcucGx1Z2luLkNvZGVnZW4u", - "UHJvY2Vzc1IHcHJvY2VzcxIoCgR3YXNtGAYgASgLMhQucGx1Z2luLkNvZGVn", - "ZW4uV0FTTVIEd2FzbRobCgdQcm9jZXNzEhAKA2NtZBgBIAEoCVIDY21kGjAK", - "BFdBU00SEAoDdXJsGAEgASgJUgN1cmwSFgoGc2hhMjU2GAIgASgJUgZzaGEy", - "NTYiiAEKB0NhdGFsb2cSGAoHY29tbWVudBgBIAEoCVIHY29tbWVudBIlCg5k", - "ZWZhdWx0X3NjaGVtYRgCIAEoCVINZGVmYXVsdFNjaGVtYRISCgRuYW1lGAMg", - "ASgJUgRuYW1lEigKB3NjaGVtYXMYBCADKAsyDi5wbHVnaW4uU2NoZW1hUgdz", - "Y2hlbWFzIsEBCgZTY2hlbWESGAoHY29tbWVudBgBIAEoCVIHY29tbWVudBIS", - "CgRuYW1lGAIgASgJUgRuYW1lEiUKBnRhYmxlcxgDIAMoCzINLnBsdWdpbi5U", - "YWJsZVIGdGFibGVzEiIKBWVudW1zGAQgAygLMgwucGx1Z2luLkVudW1SBWVu", - "dW1zEj4KD2NvbXBvc2l0ZV90eXBlcxgFIAMoCzIVLnBsdWdpbi5Db21wb3Np", - "dGVUeXBlUg5jb21wb3NpdGVUeXBlcyI9Cg1Db21wb3NpdGVUeXBlEhIKBG5h", - "bWUYASABKAlSBG5hbWUSGAoHY29tbWVudBgCIAEoCVIHY29tbWVudCJICgRF", - "bnVtEhIKBG5hbWUYASABKAlSBG5hbWUSEgoEdmFscxgCIAMoCVIEdmFscxIY", - "Cgdjb21tZW50GAMgASgJUgdjb21tZW50InEKBVRhYmxlEiQKA3JlbBgBIAEo", - "CzISLnBsdWdpbi5JZGVudGlmaWVyUgNyZWwSKAoHY29sdW1ucxgCIAMoCzIO", - "LnBsdWdpbi5Db2x1bW5SB2NvbHVtbnMSGAoHY29tbWVudBgDIAEoCVIHY29t", - "bWVudCJSCgpJZGVudGlmaWVyEhgKB2NhdGFsb2cYASABKAlSB2NhdGFsb2cS", - "FgoGc2NoZW1hGAIgASgJUgZzY2hlbWESEgoEbmFtZRgDIAEoCVIEbmFtZSKO", - "BAoGQ29sdW1uEhIKBG5hbWUYASABKAlSBG5hbWUSGQoIbm90X251bGwYAyAB", - "KAhSB25vdE51bGwSGQoIaXNfYXJyYXkYBCABKAhSB2lzQXJyYXkSGAoHY29t", - "bWVudBgFIAEoCVIHY29tbWVudBIWCgZsZW5ndGgYBiABKAVSBmxlbmd0aBIk", - "Cg5pc19uYW1lZF9wYXJhbRgHIAEoCFIMaXNOYW1lZFBhcmFtEiAKDGlzX2Z1", - "bmNfY2FsbBgIIAEoCFIKaXNGdW5jQ2FsbBIUCgVzY29wZRgJIAEoCVIFc2Nv", - "cGUSKAoFdGFibGUYCiABKAsyEi5wbHVnaW4uSWRlbnRpZmllclIFdGFibGUS", - "HwoLdGFibGVfYWxpYXMYCyABKAlSCnRhYmxlQWxpYXMSJgoEdHlwZRgMIAEo", - "CzISLnBsdWdpbi5JZGVudGlmaWVyUgR0eXBlEiIKDWlzX3NxbGNfc2xpY2UY", - "DSABKAhSC2lzU3FsY1NsaWNlEjMKC2VtYmVkX3RhYmxlGA4gASgLMhIucGx1", - "Z2luLklkZW50aWZpZXJSCmVtYmVkVGFibGUSIwoNb3JpZ2luYWxfbmFtZRgP", - "IAEoCVIMb3JpZ2luYWxOYW1lEhoKCHVuc2lnbmVkGBAgASgIUgh1bnNpZ25l", - "ZBIdCgphcnJheV9kaW1zGBEgASgFUglhcnJheURpbXMilAIKBVF1ZXJ5EhIK", - "BHRleHQYASABKAlSBHRleHQSEgoEbmFtZRgCIAEoCVIEbmFtZRIQCgNjbWQY", - "AyABKAlSA2NtZBIoCgdjb2x1bW5zGAQgAygLMg4ucGx1Z2luLkNvbHVtblIH", - "Y29sdW1ucxItCgZwYXJhbXMYBSADKAsyES5wbHVnaW4uUGFyYW1ldGVyUgpw", - "YXJhbWV0ZXJzEhoKCGNvbW1lbnRzGAYgAygJUghjb21tZW50cxIaCghmaWxl", - "bmFtZRgHIAEoCVIIZmlsZW5hbWUSQAoRaW5zZXJ0X2ludG9fdGFibGUYCCAB", - "KAsyEi5wbHVnaW4uSWRlbnRpZmllclIRaW5zZXJ0X2ludG9fdGFibGUiSwoJ", - "UGFyYW1ldGVyEhYKBm51bWJlchgBIAEoBVIGbnVtYmVyEiYKBmNvbHVtbhgC", - "IAEoCzIOLnBsdWdpbi5Db2x1bW5SBmNvbHVtbiKHAgoPR2VuZXJhdGVSZXF1", - "ZXN0EiwKCHNldHRpbmdzGAEgASgLMhAucGx1Z2luLlNldHRpbmdzUghzZXR0", - "aW5ncxIpCgdjYXRhbG9nGAIgASgLMg8ucGx1Z2luLkNhdGFsb2dSB2NhdGFs", - "b2cSJwoHcXVlcmllcxgDIAMoCzINLnBsdWdpbi5RdWVyeVIHcXVlcmllcxIi", - "CgxzcWxjX3ZlcnNpb24YBCABKAlSDHNxbGNfdmVyc2lvbhImCg5wbHVnaW5f", - "b3B0aW9ucxgFIAEoDFIOcGx1Z2luX29wdGlvbnMSJgoOZ2xvYmFsX29wdGlv", - "bnMYBiABKAxSDmdsb2JhbF9vcHRpb25zIjYKEEdlbmVyYXRlUmVzcG9uc2US", - "IgoFZmlsZXMYASADKAsyDC5wbHVnaW4uRmlsZVIFZmlsZXMyTwoOQ29kZWdl", - "blNlcnZpY2USPQoIR2VuZXJhdGUSFy5wbHVnaW4uR2VuZXJhdGVSZXF1ZXN0", - "GhgucGx1Z2luLkdlbmVyYXRlUmVzcG9uc2VCKlooZ2l0aHViLmNvbS9zcWxj", - "LWRldi9zcWxjL2ludGVybmFsL3BsdWdpbmIGcHJvdG8z")); - descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { }, - new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.File), global::Plugin.File.Parser, new[]{ "Name", "Contents" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Settings), global::Plugin.Settings.Parser, new[]{ "Version", "Engine", "Schema", "Queries", "Codegen" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Codegen), global::Plugin.Codegen.Parser, new[]{ "Out", "Plugin", "Options", "Env", "Process", "Wasm" }, null, null, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Codegen.Types.Process), global::Plugin.Codegen.Types.Process.Parser, new[]{ "Cmd" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Codegen.Types.WASM), global::Plugin.Codegen.Types.WASM.Parser, new[]{ "Url", "Sha256" }, null, null, null, null)}), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Catalog), global::Plugin.Catalog.Parser, new[]{ "Comment", "DefaultSchema", "Name", "Schemas" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Schema), global::Plugin.Schema.Parser, new[]{ "Comment", "Name", "Tables", "Enums", "CompositeTypes" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.CompositeType), global::Plugin.CompositeType.Parser, new[]{ "Name", "Comment" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Enum), global::Plugin.Enum.Parser, new[]{ "Name", "Vals", "Comment" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Table), global::Plugin.Table.Parser, new[]{ "Rel", "Columns", "Comment" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Identifier), global::Plugin.Identifier.Parser, new[]{ "Catalog", "Schema", "Name" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Column), global::Plugin.Column.Parser, new[]{ "Name", "NotNull", "IsArray", "Comment", "Length", "IsNamedParam", "IsFuncCall", "Scope", "Table", "TableAlias", "Type", "IsSqlcSlice", "EmbedTable", "OriginalName", "Unsigned", "ArrayDims" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Query), global::Plugin.Query.Parser, new[]{ "Text", "Name", "Cmd", "Columns", "Params", "Comments", "Filename", "InsertIntoTable" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.Parameter), global::Plugin.Parameter.Parser, new[]{ "Number", "Column" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.GenerateRequest), global::Plugin.GenerateRequest.Parser, new[]{ "Settings", "Catalog", "Queries", "SqlcVersion", "PluginOptions", "GlobalOptions" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Plugin.GenerateResponse), global::Plugin.GenerateResponse.Parser, new[]{ "Files" }, null, null, null, null) - })); - } - #endregion - - } - #region Messages - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class File : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new File()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[0]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public File() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public File(File other) : this() { - name_ = other.name_; - contents_ = other.contents_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public File Clone() { - return new File(this); - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 1; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "contents" field. - public const int ContentsFieldNumber = 2; - private pb::ByteString contents_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pb::ByteString Contents { - get { return contents_; } - set { - contents_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as File); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(File other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Name != other.Name) return false; - if (Contents != other.Contents) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Name.Length != 0) hash ^= Name.GetHashCode(); - if (Contents.Length != 0) hash ^= Contents.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (Contents.Length != 0) { - output.WriteRawTag(18); - output.WriteBytes(Contents); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (Contents.Length != 0) { - output.WriteRawTag(18); - output.WriteBytes(Contents); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - if (Contents.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(Contents); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(File other) { - if (other == null) { - return; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - if (other.Contents.Length != 0) { - Contents = other.Contents; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - Contents = input.ReadBytes(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - Contents = input.ReadBytes(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Settings : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Settings()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[1]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Settings() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Settings(Settings other) : this() { - version_ = other.version_; - engine_ = other.engine_; - schema_ = other.schema_.Clone(); - queries_ = other.queries_.Clone(); - codegen_ = other.codegen_ != null ? other.codegen_.Clone() : null; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Settings Clone() { - return new Settings(this); - } - - /// Field number for the "version" field. - public const int VersionFieldNumber = 1; - private string version_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Version { - get { return version_; } - set { - version_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "engine" field. - public const int EngineFieldNumber = 2; - private string engine_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Engine { - get { return engine_; } - set { - engine_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "schema" field. - public const int SchemaFieldNumber = 3; - private static readonly pb::FieldCodec _repeated_schema_codec - = pb::FieldCodec.ForString(26); - private readonly pbc::RepeatedField schema_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Schema { - get { return schema_; } - } - - /// Field number for the "queries" field. - public const int QueriesFieldNumber = 4; - private static readonly pb::FieldCodec _repeated_queries_codec - = pb::FieldCodec.ForString(34); - private readonly pbc::RepeatedField queries_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Queries { - get { return queries_; } - } - - /// Field number for the "codegen" field. - public const int CodegenFieldNumber = 12; - private global::Plugin.Codegen codegen_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Codegen Codegen { - get { return codegen_; } - set { - codegen_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Settings); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Settings other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Version != other.Version) return false; - if (Engine != other.Engine) return false; - if(!schema_.Equals(other.schema_)) return false; - if(!queries_.Equals(other.queries_)) return false; - if (!object.Equals(Codegen, other.Codegen)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Version.Length != 0) hash ^= Version.GetHashCode(); - if (Engine.Length != 0) hash ^= Engine.GetHashCode(); - hash ^= schema_.GetHashCode(); - hash ^= queries_.GetHashCode(); - if (codegen_ != null) hash ^= Codegen.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Version.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Version); - } - if (Engine.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Engine); - } - schema_.WriteTo(output, _repeated_schema_codec); - queries_.WriteTo(output, _repeated_queries_codec); - if (codegen_ != null) { - output.WriteRawTag(98); - output.WriteMessage(Codegen); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Version.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Version); - } - if (Engine.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Engine); - } - schema_.WriteTo(ref output, _repeated_schema_codec); - queries_.WriteTo(ref output, _repeated_queries_codec); - if (codegen_ != null) { - output.WriteRawTag(98); - output.WriteMessage(Codegen); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Version.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Version); - } - if (Engine.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Engine); - } - size += schema_.CalculateSize(_repeated_schema_codec); - size += queries_.CalculateSize(_repeated_queries_codec); - if (codegen_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Codegen); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Settings other) { - if (other == null) { - return; - } - if (other.Version.Length != 0) { - Version = other.Version; - } - if (other.Engine.Length != 0) { - Engine = other.Engine; - } - schema_.Add(other.schema_); - queries_.Add(other.queries_); - if (other.codegen_ != null) { - if (codegen_ == null) { - Codegen = new global::Plugin.Codegen(); - } - Codegen.MergeFrom(other.Codegen); - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Version = input.ReadString(); - break; - } - case 18: { - Engine = input.ReadString(); - break; - } - case 26: { - schema_.AddEntriesFrom(input, _repeated_schema_codec); - break; - } - case 34: { - queries_.AddEntriesFrom(input, _repeated_queries_codec); - break; - } - case 98: { - if (codegen_ == null) { - Codegen = new global::Plugin.Codegen(); - } - input.ReadMessage(Codegen); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Version = input.ReadString(); - break; - } - case 18: { - Engine = input.ReadString(); - break; - } - case 26: { - schema_.AddEntriesFrom(ref input, _repeated_schema_codec); - break; - } - case 34: { - queries_.AddEntriesFrom(ref input, _repeated_queries_codec); - break; - } - case 98: { - if (codegen_ == null) { - Codegen = new global::Plugin.Codegen(); - } - input.ReadMessage(Codegen); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Codegen : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Codegen()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[2]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Codegen() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Codegen(Codegen other) : this() { - out_ = other.out_; - plugin_ = other.plugin_; - options_ = other.options_; - env_ = other.env_.Clone(); - process_ = other.process_ != null ? other.process_.Clone() : null; - wasm_ = other.wasm_ != null ? other.wasm_.Clone() : null; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Codegen Clone() { - return new Codegen(this); - } - - /// Field number for the "out" field. - public const int OutFieldNumber = 1; - private string out_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Out { - get { return out_; } - set { - out_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "plugin" field. - public const int PluginFieldNumber = 2; - private string plugin_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Plugin { - get { return plugin_; } - set { - plugin_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "options" field. - public const int OptionsFieldNumber = 3; - private pb::ByteString options_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pb::ByteString Options { - get { return options_; } - set { - options_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "env" field. - public const int EnvFieldNumber = 4; - private static readonly pb::FieldCodec _repeated_env_codec - = pb::FieldCodec.ForString(34); - private readonly pbc::RepeatedField env_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Env { - get { return env_; } - } - - /// Field number for the "process" field. - public const int ProcessFieldNumber = 5; - private global::Plugin.Codegen.Types.Process process_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Codegen.Types.Process Process { - get { return process_; } - set { - process_ = value; - } - } - - /// Field number for the "wasm" field. - public const int WasmFieldNumber = 6; - private global::Plugin.Codegen.Types.WASM wasm_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Codegen.Types.WASM Wasm { - get { return wasm_; } - set { - wasm_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Codegen); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Codegen other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Out != other.Out) return false; - if (Plugin != other.Plugin) return false; - if (Options != other.Options) return false; - if(!env_.Equals(other.env_)) return false; - if (!object.Equals(Process, other.Process)) return false; - if (!object.Equals(Wasm, other.Wasm)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Out.Length != 0) hash ^= Out.GetHashCode(); - if (Plugin.Length != 0) hash ^= Plugin.GetHashCode(); - if (Options.Length != 0) hash ^= Options.GetHashCode(); - hash ^= env_.GetHashCode(); - if (process_ != null) hash ^= Process.GetHashCode(); - if (wasm_ != null) hash ^= Wasm.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Out.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Out); - } - if (Plugin.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Plugin); - } - if (Options.Length != 0) { - output.WriteRawTag(26); - output.WriteBytes(Options); - } - env_.WriteTo(output, _repeated_env_codec); - if (process_ != null) { - output.WriteRawTag(42); - output.WriteMessage(Process); - } - if (wasm_ != null) { - output.WriteRawTag(50); - output.WriteMessage(Wasm); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Out.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Out); - } - if (Plugin.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Plugin); - } - if (Options.Length != 0) { - output.WriteRawTag(26); - output.WriteBytes(Options); - } - env_.WriteTo(ref output, _repeated_env_codec); - if (process_ != null) { - output.WriteRawTag(42); - output.WriteMessage(Process); - } - if (wasm_ != null) { - output.WriteRawTag(50); - output.WriteMessage(Wasm); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Out.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Out); - } - if (Plugin.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Plugin); - } - if (Options.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(Options); - } - size += env_.CalculateSize(_repeated_env_codec); - if (process_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Process); - } - if (wasm_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Wasm); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Codegen other) { - if (other == null) { - return; - } - if (other.Out.Length != 0) { - Out = other.Out; - } - if (other.Plugin.Length != 0) { - Plugin = other.Plugin; - } - if (other.Options.Length != 0) { - Options = other.Options; - } - env_.Add(other.env_); - if (other.process_ != null) { - if (process_ == null) { - Process = new global::Plugin.Codegen.Types.Process(); - } - Process.MergeFrom(other.Process); - } - if (other.wasm_ != null) { - if (wasm_ == null) { - Wasm = new global::Plugin.Codegen.Types.WASM(); - } - Wasm.MergeFrom(other.Wasm); - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Out = input.ReadString(); - break; - } - case 18: { - Plugin = input.ReadString(); - break; - } - case 26: { - Options = input.ReadBytes(); - break; - } - case 34: { - env_.AddEntriesFrom(input, _repeated_env_codec); - break; - } - case 42: { - if (process_ == null) { - Process = new global::Plugin.Codegen.Types.Process(); - } - input.ReadMessage(Process); - break; - } - case 50: { - if (wasm_ == null) { - Wasm = new global::Plugin.Codegen.Types.WASM(); - } - input.ReadMessage(Wasm); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Out = input.ReadString(); - break; - } - case 18: { - Plugin = input.ReadString(); - break; - } - case 26: { - Options = input.ReadBytes(); - break; - } - case 34: { - env_.AddEntriesFrom(ref input, _repeated_env_codec); - break; - } - case 42: { - if (process_ == null) { - Process = new global::Plugin.Codegen.Types.Process(); - } - input.ReadMessage(Process); - break; - } - case 50: { - if (wasm_ == null) { - Wasm = new global::Plugin.Codegen.Types.WASM(); - } - input.ReadMessage(Wasm); - break; - } - } - } - } - #endif - - #region Nested types - /// Container for nested types declared in the Codegen message type. - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static partial class Types { - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Process : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Process()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.Codegen.Descriptor.NestedTypes[0]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Process() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Process(Process other) : this() { - cmd_ = other.cmd_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Process Clone() { - return new Process(this); - } - - /// Field number for the "cmd" field. - public const int CmdFieldNumber = 1; - private string cmd_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Cmd { - get { return cmd_; } - set { - cmd_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Process); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Process other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Cmd != other.Cmd) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Cmd.Length != 0) hash ^= Cmd.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Cmd.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Cmd); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Cmd.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Cmd); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Cmd.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Cmd); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Process other) { - if (other == null) { - return; - } - if (other.Cmd.Length != 0) { - Cmd = other.Cmd; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Cmd = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Cmd = input.ReadString(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class WASM : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new WASM()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.Codegen.Descriptor.NestedTypes[1]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public WASM() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public WASM(WASM other) : this() { - url_ = other.url_; - sha256_ = other.sha256_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public WASM Clone() { - return new WASM(this); - } - - /// Field number for the "url" field. - public const int UrlFieldNumber = 1; - private string url_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Url { - get { return url_; } - set { - url_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "sha256" field. - public const int Sha256FieldNumber = 2; - private string sha256_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Sha256 { - get { return sha256_; } - set { - sha256_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as WASM); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(WASM other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Url != other.Url) return false; - if (Sha256 != other.Sha256) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Url.Length != 0) hash ^= Url.GetHashCode(); - if (Sha256.Length != 0) hash ^= Sha256.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Url.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Url); - } - if (Sha256.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Sha256); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Url.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Url); - } - if (Sha256.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Sha256); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Url.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Url); - } - if (Sha256.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Sha256); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(WASM other) { - if (other == null) { - return; - } - if (other.Url.Length != 0) { - Url = other.Url; - } - if (other.Sha256.Length != 0) { - Sha256 = other.Sha256; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Url = input.ReadString(); - break; - } - case 18: { - Sha256 = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Url = input.ReadString(); - break; - } - case 18: { - Sha256 = input.ReadString(); - break; - } - } - } - } - #endif - - } - - } - #endregion - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Catalog : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Catalog()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[3]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Catalog() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Catalog(Catalog other) : this() { - comment_ = other.comment_; - defaultSchema_ = other.defaultSchema_; - name_ = other.name_; - schemas_ = other.schemas_.Clone(); - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Catalog Clone() { - return new Catalog(this); - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 1; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "default_schema" field. - public const int DefaultSchemaFieldNumber = 2; - private string defaultSchema_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string DefaultSchema { - get { return defaultSchema_; } - set { - defaultSchema_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 3; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "schemas" field. - public const int SchemasFieldNumber = 4; - private static readonly pb::FieldCodec _repeated_schemas_codec - = pb::FieldCodec.ForMessage(34, global::Plugin.Schema.Parser); - private readonly pbc::RepeatedField schemas_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Schemas { - get { return schemas_; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Catalog); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Catalog other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Comment != other.Comment) return false; - if (DefaultSchema != other.DefaultSchema) return false; - if (Name != other.Name) return false; - if(!schemas_.Equals(other.schemas_)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (DefaultSchema.Length != 0) hash ^= DefaultSchema.GetHashCode(); - if (Name.Length != 0) hash ^= Name.GetHashCode(); - hash ^= schemas_.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Comment.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Comment); - } - if (DefaultSchema.Length != 0) { - output.WriteRawTag(18); - output.WriteString(DefaultSchema); - } - if (Name.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Name); - } - schemas_.WriteTo(output, _repeated_schemas_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Comment.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Comment); - } - if (DefaultSchema.Length != 0) { - output.WriteRawTag(18); - output.WriteString(DefaultSchema); - } - if (Name.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Name); - } - schemas_.WriteTo(ref output, _repeated_schemas_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (DefaultSchema.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(DefaultSchema); - } - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - size += schemas_.CalculateSize(_repeated_schemas_codec); - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Catalog other) { - if (other == null) { - return; - } - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - if (other.DefaultSchema.Length != 0) { - DefaultSchema = other.DefaultSchema; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - schemas_.Add(other.schemas_); - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Comment = input.ReadString(); - break; - } - case 18: { - DefaultSchema = input.ReadString(); - break; - } - case 26: { - Name = input.ReadString(); - break; - } - case 34: { - schemas_.AddEntriesFrom(input, _repeated_schemas_codec); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Comment = input.ReadString(); - break; - } - case 18: { - DefaultSchema = input.ReadString(); - break; - } - case 26: { - Name = input.ReadString(); - break; - } - case 34: { - schemas_.AddEntriesFrom(ref input, _repeated_schemas_codec); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Schema : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Schema()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[4]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Schema() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Schema(Schema other) : this() { - comment_ = other.comment_; - name_ = other.name_; - tables_ = other.tables_.Clone(); - enums_ = other.enums_.Clone(); - compositeTypes_ = other.compositeTypes_.Clone(); - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Schema Clone() { - return new Schema(this); - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 1; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 2; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "tables" field. - public const int TablesFieldNumber = 3; - private static readonly pb::FieldCodec _repeated_tables_codec - = pb::FieldCodec.ForMessage(26, global::Plugin.Table.Parser); - private readonly pbc::RepeatedField tables_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Tables { - get { return tables_; } - } - - /// Field number for the "enums" field. - public const int EnumsFieldNumber = 4; - private static readonly pb::FieldCodec _repeated_enums_codec - = pb::FieldCodec.ForMessage(34, global::Plugin.Enum.Parser); - private readonly pbc::RepeatedField enums_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Enums { - get { return enums_; } - } - - /// Field number for the "composite_types" field. - public const int CompositeTypesFieldNumber = 5; - private static readonly pb::FieldCodec _repeated_compositeTypes_codec - = pb::FieldCodec.ForMessage(42, global::Plugin.CompositeType.Parser); - private readonly pbc::RepeatedField compositeTypes_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField CompositeTypes { - get { return compositeTypes_; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Schema); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Schema other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Comment != other.Comment) return false; - if (Name != other.Name) return false; - if(!tables_.Equals(other.tables_)) return false; - if(!enums_.Equals(other.enums_)) return false; - if(!compositeTypes_.Equals(other.compositeTypes_)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (Name.Length != 0) hash ^= Name.GetHashCode(); - hash ^= tables_.GetHashCode(); - hash ^= enums_.GetHashCode(); - hash ^= compositeTypes_.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Comment.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Comment); - } - if (Name.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Name); - } - tables_.WriteTo(output, _repeated_tables_codec); - enums_.WriteTo(output, _repeated_enums_codec); - compositeTypes_.WriteTo(output, _repeated_compositeTypes_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Comment.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Comment); - } - if (Name.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Name); - } - tables_.WriteTo(ref output, _repeated_tables_codec); - enums_.WriteTo(ref output, _repeated_enums_codec); - compositeTypes_.WriteTo(ref output, _repeated_compositeTypes_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - size += tables_.CalculateSize(_repeated_tables_codec); - size += enums_.CalculateSize(_repeated_enums_codec); - size += compositeTypes_.CalculateSize(_repeated_compositeTypes_codec); - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Schema other) { - if (other == null) { - return; - } - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - tables_.Add(other.tables_); - enums_.Add(other.enums_); - compositeTypes_.Add(other.compositeTypes_); - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Comment = input.ReadString(); - break; - } - case 18: { - Name = input.ReadString(); - break; - } - case 26: { - tables_.AddEntriesFrom(input, _repeated_tables_codec); - break; - } - case 34: { - enums_.AddEntriesFrom(input, _repeated_enums_codec); - break; - } - case 42: { - compositeTypes_.AddEntriesFrom(input, _repeated_compositeTypes_codec); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Comment = input.ReadString(); - break; - } - case 18: { - Name = input.ReadString(); - break; - } - case 26: { - tables_.AddEntriesFrom(ref input, _repeated_tables_codec); - break; - } - case 34: { - enums_.AddEntriesFrom(ref input, _repeated_enums_codec); - break; - } - case 42: { - compositeTypes_.AddEntriesFrom(ref input, _repeated_compositeTypes_codec); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class CompositeType : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new CompositeType()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[5]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public CompositeType() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public CompositeType(CompositeType other) : this() { - name_ = other.name_; - comment_ = other.comment_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public CompositeType Clone() { - return new CompositeType(this); - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 1; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 2; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as CompositeType); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(CompositeType other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Name != other.Name) return false; - if (Comment != other.Comment) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Name.Length != 0) hash ^= Name.GetHashCode(); - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (Comment.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (Comment.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(CompositeType other) { - if (other == null) { - return; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - Comment = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - Comment = input.ReadString(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Enum : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Enum()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[6]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Enum() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Enum(Enum other) : this() { - name_ = other.name_; - vals_ = other.vals_.Clone(); - comment_ = other.comment_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Enum Clone() { - return new Enum(this); - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 1; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "vals" field. - public const int ValsFieldNumber = 2; - private static readonly pb::FieldCodec _repeated_vals_codec - = pb::FieldCodec.ForString(18); - private readonly pbc::RepeatedField vals_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Vals { - get { return vals_; } - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 3; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Enum); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Enum other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Name != other.Name) return false; - if(!vals_.Equals(other.vals_)) return false; - if (Comment != other.Comment) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Name.Length != 0) hash ^= Name.GetHashCode(); - hash ^= vals_.GetHashCode(); - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - vals_.WriteTo(output, _repeated_vals_codec); - if (Comment.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - vals_.WriteTo(ref output, _repeated_vals_codec); - if (Comment.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - size += vals_.CalculateSize(_repeated_vals_codec); - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Enum other) { - if (other == null) { - return; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - vals_.Add(other.vals_); - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - vals_.AddEntriesFrom(input, _repeated_vals_codec); - break; - } - case 26: { - Comment = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 18: { - vals_.AddEntriesFrom(ref input, _repeated_vals_codec); - break; - } - case 26: { - Comment = input.ReadString(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Table : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser
_parser = new pb::MessageParser
(() => new Table()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser
Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[7]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Table() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Table(Table other) : this() { - rel_ = other.rel_ != null ? other.rel_.Clone() : null; - columns_ = other.columns_.Clone(); - comment_ = other.comment_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Table Clone() { - return new Table(this); - } - - /// Field number for the "rel" field. - public const int RelFieldNumber = 1; - private global::Plugin.Identifier rel_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Identifier Rel { - get { return rel_; } - set { - rel_ = value; - } - } - - /// Field number for the "columns" field. - public const int ColumnsFieldNumber = 2; - private static readonly pb::FieldCodec _repeated_columns_codec - = pb::FieldCodec.ForMessage(18, global::Plugin.Column.Parser); - private readonly pbc::RepeatedField columns_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Columns { - get { return columns_; } - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 3; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Table); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Table other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (!object.Equals(Rel, other.Rel)) return false; - if(!columns_.Equals(other.columns_)) return false; - if (Comment != other.Comment) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (rel_ != null) hash ^= Rel.GetHashCode(); - hash ^= columns_.GetHashCode(); - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (rel_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Rel); - } - columns_.WriteTo(output, _repeated_columns_codec); - if (Comment.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (rel_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Rel); - } - columns_.WriteTo(ref output, _repeated_columns_codec); - if (Comment.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Comment); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (rel_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Rel); - } - size += columns_.CalculateSize(_repeated_columns_codec); - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Table other) { - if (other == null) { - return; - } - if (other.rel_ != null) { - if (rel_ == null) { - Rel = new global::Plugin.Identifier(); - } - Rel.MergeFrom(other.Rel); - } - columns_.Add(other.columns_); - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - if (rel_ == null) { - Rel = new global::Plugin.Identifier(); - } - input.ReadMessage(Rel); - break; - } - case 18: { - columns_.AddEntriesFrom(input, _repeated_columns_codec); - break; - } - case 26: { - Comment = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - if (rel_ == null) { - Rel = new global::Plugin.Identifier(); - } - input.ReadMessage(Rel); - break; - } - case 18: { - columns_.AddEntriesFrom(ref input, _repeated_columns_codec); - break; - } - case 26: { - Comment = input.ReadString(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Identifier : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Identifier()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[8]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Identifier() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Identifier(Identifier other) : this() { - catalog_ = other.catalog_; - schema_ = other.schema_; - name_ = other.name_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Identifier Clone() { - return new Identifier(this); - } - - /// Field number for the "catalog" field. - public const int CatalogFieldNumber = 1; - private string catalog_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Catalog { - get { return catalog_; } - set { - catalog_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "schema" field. - public const int SchemaFieldNumber = 2; - private string schema_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Schema { - get { return schema_; } - set { - schema_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 3; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Identifier); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Identifier other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Catalog != other.Catalog) return false; - if (Schema != other.Schema) return false; - if (Name != other.Name) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Catalog.Length != 0) hash ^= Catalog.GetHashCode(); - if (Schema.Length != 0) hash ^= Schema.GetHashCode(); - if (Name.Length != 0) hash ^= Name.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Catalog.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Catalog); - } - if (Schema.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Schema); - } - if (Name.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Name); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Catalog.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Catalog); - } - if (Schema.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Schema); - } - if (Name.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Name); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Catalog.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Catalog); - } - if (Schema.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Schema); - } - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Identifier other) { - if (other == null) { - return; - } - if (other.Catalog.Length != 0) { - Catalog = other.Catalog; - } - if (other.Schema.Length != 0) { - Schema = other.Schema; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Catalog = input.ReadString(); - break; - } - case 18: { - Schema = input.ReadString(); - break; - } - case 26: { - Name = input.ReadString(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Catalog = input.ReadString(); - break; - } - case 18: { - Schema = input.ReadString(); - break; - } - case 26: { - Name = input.ReadString(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Column : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Column()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[9]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Column() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Column(Column other) : this() { - name_ = other.name_; - notNull_ = other.notNull_; - isArray_ = other.isArray_; - comment_ = other.comment_; - length_ = other.length_; - isNamedParam_ = other.isNamedParam_; - isFuncCall_ = other.isFuncCall_; - scope_ = other.scope_; - table_ = other.table_ != null ? other.table_.Clone() : null; - tableAlias_ = other.tableAlias_; - type_ = other.type_ != null ? other.type_.Clone() : null; - isSqlcSlice_ = other.isSqlcSlice_; - embedTable_ = other.embedTable_ != null ? other.embedTable_.Clone() : null; - originalName_ = other.originalName_; - unsigned_ = other.unsigned_; - arrayDims_ = other.arrayDims_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Column Clone() { - return new Column(this); - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 1; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "not_null" field. - public const int NotNullFieldNumber = 3; - private bool notNull_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool NotNull { - get { return notNull_; } - set { - notNull_ = value; - } - } - - /// Field number for the "is_array" field. - public const int IsArrayFieldNumber = 4; - private bool isArray_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool IsArray { - get { return isArray_; } - set { - isArray_ = value; - } - } - - /// Field number for the "comment" field. - public const int CommentFieldNumber = 5; - private string comment_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Comment { - get { return comment_; } - set { - comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "length" field. - public const int LengthFieldNumber = 6; - private int length_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int Length { - get { return length_; } - set { - length_ = value; - } - } - - /// Field number for the "is_named_param" field. - public const int IsNamedParamFieldNumber = 7; - private bool isNamedParam_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool IsNamedParam { - get { return isNamedParam_; } - set { - isNamedParam_ = value; - } - } - - /// Field number for the "is_func_call" field. - public const int IsFuncCallFieldNumber = 8; - private bool isFuncCall_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool IsFuncCall { - get { return isFuncCall_; } - set { - isFuncCall_ = value; - } - } - - /// Field number for the "scope" field. - public const int ScopeFieldNumber = 9; - private string scope_ = ""; - /// - /// XXX: Figure out what PostgreSQL calls `foo.id` - /// - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Scope { - get { return scope_; } - set { - scope_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "table" field. - public const int TableFieldNumber = 10; - private global::Plugin.Identifier table_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Identifier Table { - get { return table_; } - set { - table_ = value; - } - } - - /// Field number for the "table_alias" field. - public const int TableAliasFieldNumber = 11; - private string tableAlias_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string TableAlias { - get { return tableAlias_; } - set { - tableAlias_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "type" field. - public const int TypeFieldNumber = 12; - private global::Plugin.Identifier type_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Identifier Type { - get { return type_; } - set { - type_ = value; - } - } - - /// Field number for the "is_sqlc_slice" field. - public const int IsSqlcSliceFieldNumber = 13; - private bool isSqlcSlice_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool IsSqlcSlice { - get { return isSqlcSlice_; } - set { - isSqlcSlice_ = value; - } - } - - /// Field number for the "embed_table" field. - public const int EmbedTableFieldNumber = 14; - private global::Plugin.Identifier embedTable_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Identifier EmbedTable { - get { return embedTable_; } - set { - embedTable_ = value; - } - } - - /// Field number for the "original_name" field. - public const int OriginalNameFieldNumber = 15; - private string originalName_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string OriginalName { - get { return originalName_; } - set { - originalName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "unsigned" field. - public const int UnsignedFieldNumber = 16; - private bool unsigned_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Unsigned { - get { return unsigned_; } - set { - unsigned_ = value; - } - } - - /// Field number for the "array_dims" field. - public const int ArrayDimsFieldNumber = 17; - private int arrayDims_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int ArrayDims { - get { return arrayDims_; } - set { - arrayDims_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Column); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Column other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Name != other.Name) return false; - if (NotNull != other.NotNull) return false; - if (IsArray != other.IsArray) return false; - if (Comment != other.Comment) return false; - if (Length != other.Length) return false; - if (IsNamedParam != other.IsNamedParam) return false; - if (IsFuncCall != other.IsFuncCall) return false; - if (Scope != other.Scope) return false; - if (!object.Equals(Table, other.Table)) return false; - if (TableAlias != other.TableAlias) return false; - if (!object.Equals(Type, other.Type)) return false; - if (IsSqlcSlice != other.IsSqlcSlice) return false; - if (!object.Equals(EmbedTable, other.EmbedTable)) return false; - if (OriginalName != other.OriginalName) return false; - if (Unsigned != other.Unsigned) return false; - if (ArrayDims != other.ArrayDims) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Name.Length != 0) hash ^= Name.GetHashCode(); - if (NotNull != false) hash ^= NotNull.GetHashCode(); - if (IsArray != false) hash ^= IsArray.GetHashCode(); - if (Comment.Length != 0) hash ^= Comment.GetHashCode(); - if (Length != 0) hash ^= Length.GetHashCode(); - if (IsNamedParam != false) hash ^= IsNamedParam.GetHashCode(); - if (IsFuncCall != false) hash ^= IsFuncCall.GetHashCode(); - if (Scope.Length != 0) hash ^= Scope.GetHashCode(); - if (table_ != null) hash ^= Table.GetHashCode(); - if (TableAlias.Length != 0) hash ^= TableAlias.GetHashCode(); - if (type_ != null) hash ^= Type.GetHashCode(); - if (IsSqlcSlice != false) hash ^= IsSqlcSlice.GetHashCode(); - if (embedTable_ != null) hash ^= EmbedTable.GetHashCode(); - if (OriginalName.Length != 0) hash ^= OriginalName.GetHashCode(); - if (Unsigned != false) hash ^= Unsigned.GetHashCode(); - if (ArrayDims != 0) hash ^= ArrayDims.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (NotNull != false) { - output.WriteRawTag(24); - output.WriteBool(NotNull); - } - if (IsArray != false) { - output.WriteRawTag(32); - output.WriteBool(IsArray); - } - if (Comment.Length != 0) { - output.WriteRawTag(42); - output.WriteString(Comment); - } - if (Length != 0) { - output.WriteRawTag(48); - output.WriteInt32(Length); - } - if (IsNamedParam != false) { - output.WriteRawTag(56); - output.WriteBool(IsNamedParam); - } - if (IsFuncCall != false) { - output.WriteRawTag(64); - output.WriteBool(IsFuncCall); - } - if (Scope.Length != 0) { - output.WriteRawTag(74); - output.WriteString(Scope); - } - if (table_ != null) { - output.WriteRawTag(82); - output.WriteMessage(Table); - } - if (TableAlias.Length != 0) { - output.WriteRawTag(90); - output.WriteString(TableAlias); - } - if (type_ != null) { - output.WriteRawTag(98); - output.WriteMessage(Type); - } - if (IsSqlcSlice != false) { - output.WriteRawTag(104); - output.WriteBool(IsSqlcSlice); - } - if (embedTable_ != null) { - output.WriteRawTag(114); - output.WriteMessage(EmbedTable); - } - if (OriginalName.Length != 0) { - output.WriteRawTag(122); - output.WriteString(OriginalName); - } - if (Unsigned != false) { - output.WriteRawTag(128, 1); - output.WriteBool(Unsigned); - } - if (ArrayDims != 0) { - output.WriteRawTag(136, 1); - output.WriteInt32(ArrayDims); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Name.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Name); - } - if (NotNull != false) { - output.WriteRawTag(24); - output.WriteBool(NotNull); - } - if (IsArray != false) { - output.WriteRawTag(32); - output.WriteBool(IsArray); - } - if (Comment.Length != 0) { - output.WriteRawTag(42); - output.WriteString(Comment); - } - if (Length != 0) { - output.WriteRawTag(48); - output.WriteInt32(Length); - } - if (IsNamedParam != false) { - output.WriteRawTag(56); - output.WriteBool(IsNamedParam); - } - if (IsFuncCall != false) { - output.WriteRawTag(64); - output.WriteBool(IsFuncCall); - } - if (Scope.Length != 0) { - output.WriteRawTag(74); - output.WriteString(Scope); - } - if (table_ != null) { - output.WriteRawTag(82); - output.WriteMessage(Table); - } - if (TableAlias.Length != 0) { - output.WriteRawTag(90); - output.WriteString(TableAlias); - } - if (type_ != null) { - output.WriteRawTag(98); - output.WriteMessage(Type); - } - if (IsSqlcSlice != false) { - output.WriteRawTag(104); - output.WriteBool(IsSqlcSlice); - } - if (embedTable_ != null) { - output.WriteRawTag(114); - output.WriteMessage(EmbedTable); - } - if (OriginalName.Length != 0) { - output.WriteRawTag(122); - output.WriteString(OriginalName); - } - if (Unsigned != false) { - output.WriteRawTag(128, 1); - output.WriteBool(Unsigned); - } - if (ArrayDims != 0) { - output.WriteRawTag(136, 1); - output.WriteInt32(ArrayDims); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - if (NotNull != false) { - size += 1 + 1; - } - if (IsArray != false) { - size += 1 + 1; - } - if (Comment.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); - } - if (Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeInt32Size(Length); - } - if (IsNamedParam != false) { - size += 1 + 1; - } - if (IsFuncCall != false) { - size += 1 + 1; - } - if (Scope.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Scope); - } - if (table_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Table); - } - if (TableAlias.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(TableAlias); - } - if (type_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Type); - } - if (IsSqlcSlice != false) { - size += 1 + 1; - } - if (embedTable_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(EmbedTable); - } - if (OriginalName.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(OriginalName); - } - if (Unsigned != false) { - size += 2 + 1; - } - if (ArrayDims != 0) { - size += 2 + pb::CodedOutputStream.ComputeInt32Size(ArrayDims); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Column other) { - if (other == null) { - return; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - if (other.NotNull != false) { - NotNull = other.NotNull; - } - if (other.IsArray != false) { - IsArray = other.IsArray; - } - if (other.Comment.Length != 0) { - Comment = other.Comment; - } - if (other.Length != 0) { - Length = other.Length; - } - if (other.IsNamedParam != false) { - IsNamedParam = other.IsNamedParam; - } - if (other.IsFuncCall != false) { - IsFuncCall = other.IsFuncCall; - } - if (other.Scope.Length != 0) { - Scope = other.Scope; - } - if (other.table_ != null) { - if (table_ == null) { - Table = new global::Plugin.Identifier(); - } - Table.MergeFrom(other.Table); - } - if (other.TableAlias.Length != 0) { - TableAlias = other.TableAlias; - } - if (other.type_ != null) { - if (type_ == null) { - Type = new global::Plugin.Identifier(); - } - Type.MergeFrom(other.Type); - } - if (other.IsSqlcSlice != false) { - IsSqlcSlice = other.IsSqlcSlice; - } - if (other.embedTable_ != null) { - if (embedTable_ == null) { - EmbedTable = new global::Plugin.Identifier(); - } - EmbedTable.MergeFrom(other.EmbedTable); - } - if (other.OriginalName.Length != 0) { - OriginalName = other.OriginalName; - } - if (other.Unsigned != false) { - Unsigned = other.Unsigned; - } - if (other.ArrayDims != 0) { - ArrayDims = other.ArrayDims; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 24: { - NotNull = input.ReadBool(); - break; - } - case 32: { - IsArray = input.ReadBool(); - break; - } - case 42: { - Comment = input.ReadString(); - break; - } - case 48: { - Length = input.ReadInt32(); - break; - } - case 56: { - IsNamedParam = input.ReadBool(); - break; - } - case 64: { - IsFuncCall = input.ReadBool(); - break; - } - case 74: { - Scope = input.ReadString(); - break; - } - case 82: { - if (table_ == null) { - Table = new global::Plugin.Identifier(); - } - input.ReadMessage(Table); - break; - } - case 90: { - TableAlias = input.ReadString(); - break; - } - case 98: { - if (type_ == null) { - Type = new global::Plugin.Identifier(); - } - input.ReadMessage(Type); - break; - } - case 104: { - IsSqlcSlice = input.ReadBool(); - break; - } - case 114: { - if (embedTable_ == null) { - EmbedTable = new global::Plugin.Identifier(); - } - input.ReadMessage(EmbedTable); - break; - } - case 122: { - OriginalName = input.ReadString(); - break; - } - case 128: { - Unsigned = input.ReadBool(); - break; - } - case 136: { - ArrayDims = input.ReadInt32(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Name = input.ReadString(); - break; - } - case 24: { - NotNull = input.ReadBool(); - break; - } - case 32: { - IsArray = input.ReadBool(); - break; - } - case 42: { - Comment = input.ReadString(); - break; - } - case 48: { - Length = input.ReadInt32(); - break; - } - case 56: { - IsNamedParam = input.ReadBool(); - break; - } - case 64: { - IsFuncCall = input.ReadBool(); - break; - } - case 74: { - Scope = input.ReadString(); - break; - } - case 82: { - if (table_ == null) { - Table = new global::Plugin.Identifier(); - } - input.ReadMessage(Table); - break; - } - case 90: { - TableAlias = input.ReadString(); - break; - } - case 98: { - if (type_ == null) { - Type = new global::Plugin.Identifier(); - } - input.ReadMessage(Type); - break; - } - case 104: { - IsSqlcSlice = input.ReadBool(); - break; - } - case 114: { - if (embedTable_ == null) { - EmbedTable = new global::Plugin.Identifier(); - } - input.ReadMessage(EmbedTable); - break; - } - case 122: { - OriginalName = input.ReadString(); - break; - } - case 128: { - Unsigned = input.ReadBool(); - break; - } - case 136: { - ArrayDims = input.ReadInt32(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Query : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Query()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[10]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Query() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Query(Query other) : this() { - text_ = other.text_; - name_ = other.name_; - cmd_ = other.cmd_; - columns_ = other.columns_.Clone(); - params_ = other.params_.Clone(); - comments_ = other.comments_.Clone(); - filename_ = other.filename_; - insertIntoTable_ = other.insertIntoTable_ != null ? other.insertIntoTable_.Clone() : null; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Query Clone() { - return new Query(this); - } - - /// Field number for the "text" field. - public const int TextFieldNumber = 1; - private string text_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Text { - get { return text_; } - set { - text_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "name" field. - public const int NameFieldNumber = 2; - private string name_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Name { - get { return name_; } - set { - name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "cmd" field. - public const int CmdFieldNumber = 3; - private string cmd_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Cmd { - get { return cmd_; } - set { - cmd_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "columns" field. - public const int ColumnsFieldNumber = 4; - private static readonly pb::FieldCodec _repeated_columns_codec - = pb::FieldCodec.ForMessage(34, global::Plugin.Column.Parser); - private readonly pbc::RepeatedField columns_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Columns { - get { return columns_; } - } - - /// Field number for the "params" field. - public const int ParamsFieldNumber = 5; - private static readonly pb::FieldCodec _repeated_params_codec - = pb::FieldCodec.ForMessage(42, global::Plugin.Parameter.Parser); - private readonly pbc::RepeatedField params_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Params { - get { return params_; } - } - - /// Field number for the "comments" field. - public const int CommentsFieldNumber = 6; - private static readonly pb::FieldCodec _repeated_comments_codec - = pb::FieldCodec.ForString(50); - private readonly pbc::RepeatedField comments_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Comments { - get { return comments_; } - } - - /// Field number for the "filename" field. - public const int FilenameFieldNumber = 7; - private string filename_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string Filename { - get { return filename_; } - set { - filename_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "insert_into_table" field. - public const int InsertIntoTableFieldNumber = 8; - private global::Plugin.Identifier insertIntoTable_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Identifier InsertIntoTable { - get { return insertIntoTable_; } - set { - insertIntoTable_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Query); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Query other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Text != other.Text) return false; - if (Name != other.Name) return false; - if (Cmd != other.Cmd) return false; - if(!columns_.Equals(other.columns_)) return false; - if(!params_.Equals(other.params_)) return false; - if(!comments_.Equals(other.comments_)) return false; - if (Filename != other.Filename) return false; - if (!object.Equals(InsertIntoTable, other.InsertIntoTable)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Text.Length != 0) hash ^= Text.GetHashCode(); - if (Name.Length != 0) hash ^= Name.GetHashCode(); - if (Cmd.Length != 0) hash ^= Cmd.GetHashCode(); - hash ^= columns_.GetHashCode(); - hash ^= params_.GetHashCode(); - hash ^= comments_.GetHashCode(); - if (Filename.Length != 0) hash ^= Filename.GetHashCode(); - if (insertIntoTable_ != null) hash ^= InsertIntoTable.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Text.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Text); - } - if (Name.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Name); - } - if (Cmd.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Cmd); - } - columns_.WriteTo(output, _repeated_columns_codec); - params_.WriteTo(output, _repeated_params_codec); - comments_.WriteTo(output, _repeated_comments_codec); - if (Filename.Length != 0) { - output.WriteRawTag(58); - output.WriteString(Filename); - } - if (insertIntoTable_ != null) { - output.WriteRawTag(66); - output.WriteMessage(InsertIntoTable); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Text.Length != 0) { - output.WriteRawTag(10); - output.WriteString(Text); - } - if (Name.Length != 0) { - output.WriteRawTag(18); - output.WriteString(Name); - } - if (Cmd.Length != 0) { - output.WriteRawTag(26); - output.WriteString(Cmd); - } - columns_.WriteTo(ref output, _repeated_columns_codec); - params_.WriteTo(ref output, _repeated_params_codec); - comments_.WriteTo(ref output, _repeated_comments_codec); - if (Filename.Length != 0) { - output.WriteRawTag(58); - output.WriteString(Filename); - } - if (insertIntoTable_ != null) { - output.WriteRawTag(66); - output.WriteMessage(InsertIntoTable); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Text.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Text); - } - if (Name.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); - } - if (Cmd.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Cmd); - } - size += columns_.CalculateSize(_repeated_columns_codec); - size += params_.CalculateSize(_repeated_params_codec); - size += comments_.CalculateSize(_repeated_comments_codec); - if (Filename.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(Filename); - } - if (insertIntoTable_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(InsertIntoTable); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Query other) { - if (other == null) { - return; - } - if (other.Text.Length != 0) { - Text = other.Text; - } - if (other.Name.Length != 0) { - Name = other.Name; - } - if (other.Cmd.Length != 0) { - Cmd = other.Cmd; - } - columns_.Add(other.columns_); - params_.Add(other.params_); - comments_.Add(other.comments_); - if (other.Filename.Length != 0) { - Filename = other.Filename; - } - if (other.insertIntoTable_ != null) { - if (insertIntoTable_ == null) { - InsertIntoTable = new global::Plugin.Identifier(); - } - InsertIntoTable.MergeFrom(other.InsertIntoTable); - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - Text = input.ReadString(); - break; - } - case 18: { - Name = input.ReadString(); - break; - } - case 26: { - Cmd = input.ReadString(); - break; - } - case 34: { - columns_.AddEntriesFrom(input, _repeated_columns_codec); - break; - } - case 42: { - params_.AddEntriesFrom(input, _repeated_params_codec); - break; - } - case 50: { - comments_.AddEntriesFrom(input, _repeated_comments_codec); - break; - } - case 58: { - Filename = input.ReadString(); - break; - } - case 66: { - if (insertIntoTable_ == null) { - InsertIntoTable = new global::Plugin.Identifier(); - } - input.ReadMessage(InsertIntoTable); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - Text = input.ReadString(); - break; - } - case 18: { - Name = input.ReadString(); - break; - } - case 26: { - Cmd = input.ReadString(); - break; - } - case 34: { - columns_.AddEntriesFrom(ref input, _repeated_columns_codec); - break; - } - case 42: { - params_.AddEntriesFrom(ref input, _repeated_params_codec); - break; - } - case 50: { - comments_.AddEntriesFrom(ref input, _repeated_comments_codec); - break; - } - case 58: { - Filename = input.ReadString(); - break; - } - case 66: { - if (insertIntoTable_ == null) { - InsertIntoTable = new global::Plugin.Identifier(); - } - input.ReadMessage(InsertIntoTable); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class Parameter : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Parameter()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[11]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Parameter() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Parameter(Parameter other) : this() { - number_ = other.number_; - column_ = other.column_ != null ? other.column_.Clone() : null; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public Parameter Clone() { - return new Parameter(this); - } - - /// Field number for the "number" field. - public const int NumberFieldNumber = 1; - private int number_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int Number { - get { return number_; } - set { - number_ = value; - } - } - - /// Field number for the "column" field. - public const int ColumnFieldNumber = 2; - private global::Plugin.Column column_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Column Column { - get { return column_; } - set { - column_ = value; - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as Parameter); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(Parameter other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (Number != other.Number) return false; - if (!object.Equals(Column, other.Column)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (Number != 0) hash ^= Number.GetHashCode(); - if (column_ != null) hash ^= Column.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (Number != 0) { - output.WriteRawTag(8); - output.WriteInt32(Number); - } - if (column_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Column); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Number != 0) { - output.WriteRawTag(8); - output.WriteInt32(Number); - } - if (column_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Column); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (Number != 0) { - size += 1 + pb::CodedOutputStream.ComputeInt32Size(Number); - } - if (column_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Column); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(Parameter other) { - if (other == null) { - return; - } - if (other.Number != 0) { - Number = other.Number; - } - if (other.column_ != null) { - if (column_ == null) { - Column = new global::Plugin.Column(); - } - Column.MergeFrom(other.Column); - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 8: { - Number = input.ReadInt32(); - break; - } - case 18: { - if (column_ == null) { - Column = new global::Plugin.Column(); - } - input.ReadMessage(Column); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 8: { - Number = input.ReadInt32(); - break; - } - case 18: { - if (column_ == null) { - Column = new global::Plugin.Column(); - } - input.ReadMessage(Column); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class GenerateRequest : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GenerateRequest()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[12]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateRequest() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateRequest(GenerateRequest other) : this() { - settings_ = other.settings_ != null ? other.settings_.Clone() : null; - catalog_ = other.catalog_ != null ? other.catalog_.Clone() : null; - queries_ = other.queries_.Clone(); - sqlcVersion_ = other.sqlcVersion_; - pluginOptions_ = other.pluginOptions_; - globalOptions_ = other.globalOptions_; - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateRequest Clone() { - return new GenerateRequest(this); - } - - /// Field number for the "settings" field. - public const int SettingsFieldNumber = 1; - private global::Plugin.Settings settings_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Settings Settings { - get { return settings_; } - set { - settings_ = value; - } - } - - /// Field number for the "catalog" field. - public const int CatalogFieldNumber = 2; - private global::Plugin.Catalog catalog_; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public global::Plugin.Catalog Catalog { - get { return catalog_; } - set { - catalog_ = value; - } - } - - /// Field number for the "queries" field. - public const int QueriesFieldNumber = 3; - private static readonly pb::FieldCodec _repeated_queries_codec - = pb::FieldCodec.ForMessage(26, global::Plugin.Query.Parser); - private readonly pbc::RepeatedField queries_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Queries { - get { return queries_; } - } - - /// Field number for the "sqlc_version" field. - public const int SqlcVersionFieldNumber = 4; - private string sqlcVersion_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string SqlcVersion { - get { return sqlcVersion_; } - set { - sqlcVersion_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "plugin_options" field. - public const int PluginOptionsFieldNumber = 5; - private pb::ByteString pluginOptions_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pb::ByteString PluginOptions { - get { return pluginOptions_; } - set { - pluginOptions_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - /// Field number for the "global_options" field. - public const int GlobalOptionsFieldNumber = 6; - private pb::ByteString globalOptions_ = pb::ByteString.Empty; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pb::ByteString GlobalOptions { - get { return globalOptions_; } - set { - globalOptions_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as GenerateRequest); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(GenerateRequest other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if (!object.Equals(Settings, other.Settings)) return false; - if (!object.Equals(Catalog, other.Catalog)) return false; - if(!queries_.Equals(other.queries_)) return false; - if (SqlcVersion != other.SqlcVersion) return false; - if (PluginOptions != other.PluginOptions) return false; - if (GlobalOptions != other.GlobalOptions) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - if (settings_ != null) hash ^= Settings.GetHashCode(); - if (catalog_ != null) hash ^= Catalog.GetHashCode(); - hash ^= queries_.GetHashCode(); - if (SqlcVersion.Length != 0) hash ^= SqlcVersion.GetHashCode(); - if (PluginOptions.Length != 0) hash ^= PluginOptions.GetHashCode(); - if (GlobalOptions.Length != 0) hash ^= GlobalOptions.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - if (settings_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Settings); - } - if (catalog_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Catalog); - } - queries_.WriteTo(output, _repeated_queries_codec); - if (SqlcVersion.Length != 0) { - output.WriteRawTag(34); - output.WriteString(SqlcVersion); - } - if (PluginOptions.Length != 0) { - output.WriteRawTag(42); - output.WriteBytes(PluginOptions); - } - if (GlobalOptions.Length != 0) { - output.WriteRawTag(50); - output.WriteBytes(GlobalOptions); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (settings_ != null) { - output.WriteRawTag(10); - output.WriteMessage(Settings); - } - if (catalog_ != null) { - output.WriteRawTag(18); - output.WriteMessage(Catalog); - } - queries_.WriteTo(ref output, _repeated_queries_codec); - if (SqlcVersion.Length != 0) { - output.WriteRawTag(34); - output.WriteString(SqlcVersion); - } - if (PluginOptions.Length != 0) { - output.WriteRawTag(42); - output.WriteBytes(PluginOptions); - } - if (GlobalOptions.Length != 0) { - output.WriteRawTag(50); - output.WriteBytes(GlobalOptions); - } - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - if (settings_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Settings); - } - if (catalog_ != null) { - size += 1 + pb::CodedOutputStream.ComputeMessageSize(Catalog); - } - size += queries_.CalculateSize(_repeated_queries_codec); - if (SqlcVersion.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(SqlcVersion); - } - if (PluginOptions.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(PluginOptions); - } - if (GlobalOptions.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeBytesSize(GlobalOptions); - } - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(GenerateRequest other) { - if (other == null) { - return; - } - if (other.settings_ != null) { - if (settings_ == null) { - Settings = new global::Plugin.Settings(); - } - Settings.MergeFrom(other.Settings); - } - if (other.catalog_ != null) { - if (catalog_ == null) { - Catalog = new global::Plugin.Catalog(); - } - Catalog.MergeFrom(other.Catalog); - } - queries_.Add(other.queries_); - if (other.SqlcVersion.Length != 0) { - SqlcVersion = other.SqlcVersion; - } - if (other.PluginOptions.Length != 0) { - PluginOptions = other.PluginOptions; - } - if (other.GlobalOptions.Length != 0) { - GlobalOptions = other.GlobalOptions; - } - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - if (settings_ == null) { - Settings = new global::Plugin.Settings(); - } - input.ReadMessage(Settings); - break; - } - case 18: { - if (catalog_ == null) { - Catalog = new global::Plugin.Catalog(); - } - input.ReadMessage(Catalog); - break; - } - case 26: { - queries_.AddEntriesFrom(input, _repeated_queries_codec); - break; - } - case 34: { - SqlcVersion = input.ReadString(); - break; - } - case 42: { - PluginOptions = input.ReadBytes(); - break; - } - case 50: { - GlobalOptions = input.ReadBytes(); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - if (settings_ == null) { - Settings = new global::Plugin.Settings(); - } - input.ReadMessage(Settings); - break; - } - case 18: { - if (catalog_ == null) { - Catalog = new global::Plugin.Catalog(); - } - input.ReadMessage(Catalog); - break; - } - case 26: { - queries_.AddEntriesFrom(ref input, _repeated_queries_codec); - break; - } - case 34: { - SqlcVersion = input.ReadString(); - break; - } - case 42: { - PluginOptions = input.ReadBytes(); - break; - } - case 50: { - GlobalOptions = input.ReadBytes(); - break; - } - } - } - } - #endif - - } - - [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class GenerateResponse : pb::IMessage - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - , pb::IBufferMessage - #endif - { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GenerateResponse()); - private pb::UnknownFieldSet _unknownFields; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pbr::MessageDescriptor Descriptor { - get { return global::Plugin.CodegenReflection.Descriptor.MessageTypes[13]; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - pbr::MessageDescriptor pb::IMessage.Descriptor { - get { return Descriptor; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateResponse() { - OnConstruction(); - } - - partial void OnConstruction(); - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateResponse(GenerateResponse other) : this() { - files_ = other.files_.Clone(); - _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public GenerateResponse Clone() { - return new GenerateResponse(this); - } - - /// Field number for the "files" field. - public const int FilesFieldNumber = 1; - private static readonly pb::FieldCodec _repeated_files_codec - = pb::FieldCodec.ForMessage(10, global::Plugin.File.Parser); - private readonly pbc::RepeatedField files_ = new pbc::RepeatedField(); - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Files { - get { return files_; } - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override bool Equals(object other) { - return Equals(other as GenerateResponse); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(GenerateResponse other) { - if (ReferenceEquals(other, null)) { - return false; - } - if (ReferenceEquals(other, this)) { - return true; - } - if(!files_.Equals(other.files_)) return false; - return Equals(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override int GetHashCode() { - int hash = 1; - hash ^= files_.GetHashCode(); - if (_unknownFields != null) { - hash ^= _unknownFields.GetHashCode(); - } - return hash; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public override string ToString() { - return pb::JsonFormatter.ToDiagnosticString(this); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void WriteTo(pb::CodedOutputStream output) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - output.WriteRawMessage(this); - #else - files_.WriteTo(output, _repeated_files_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(output); - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - files_.WriteTo(ref output, _repeated_files_codec); - if (_unknownFields != null) { - _unknownFields.WriteTo(ref output); - } - } - #endif - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public int CalculateSize() { - int size = 0; - size += files_.CalculateSize(_repeated_files_codec); - if (_unknownFields != null) { - size += _unknownFields.CalculateSize(); - } - return size; - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(GenerateResponse other) { - if (other == null) { - return; - } - files_.Add(other.files_); - _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); - } - - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(pb::CodedInputStream input) { - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - input.ReadRawMessage(this); - #else - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); - break; - case 10: { - files_.AddEntriesFrom(input, _repeated_files_codec); - break; - } - } - } - #endif - } - - #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { - uint tag; - while ((tag = input.ReadTag()) != 0) { - if ((tag & 7) == 4) { - // Abort on any end group tag. - return; - } - switch(tag) { - default: - _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); - break; - case 10: { - files_.AddEntriesFrom(ref input, _repeated_files_codec); - break; - } - } - } - } - #endif - - } - - #endregion - -} - -#endregion Designer generated code diff --git a/GeneratedProtobuf/GeneratedProtobuf.csproj b/GeneratedProtobuf/GeneratedProtobuf.csproj deleted file mode 100644 index 0fcfa08..0000000 --- a/GeneratedProtobuf/GeneratedProtobuf.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net8.0 - enable - SqlcGenRuby - - - - - - - - diff --git a/LocalRunner/App.cs b/LocalRunner/App.cs deleted file mode 100644 index 62485e9..0000000 --- a/LocalRunner/App.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SqlcGenRuby; - -public static class App -{ - public static void Main() - { - PluginRunner.Run(); - } -} \ No newline at end of file diff --git a/LocalRunner/LocalRunner.csproj b/LocalRunner/LocalRunner.csproj deleted file mode 100644 index a3c959a..0000000 --- a/LocalRunner/LocalRunner.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net8.0 - true - osx-arm64;linux-x64 - SqlcGenRuby - - - - - - - diff --git a/LocalRunner/Properties/AssemblyInfo.cs b/LocalRunner/Properties/AssemblyInfo.cs deleted file mode 100644 index f1455cc..0000000 --- a/LocalRunner/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.Versioning; - -[assembly: SupportedOSPlatform("wasi")] \ No newline at end of file diff --git a/LocalRunner/nuget.config b/LocalRunner/nuget.config deleted file mode 100644 index 26376ec..0000000 --- a/LocalRunner/nuget.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/PluginOptions/Options.cs b/PluginOptions/Options.cs deleted file mode 100644 index f1088e2..0000000 --- a/PluginOptions/Options.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Plugin; -using System; -using System.Text; -using System.Text.Json; -using Enum = System.Enum; - -namespace SqlcGenRuby; - -public enum DriverName -{ - Mysql2, - Pg, - Sqlite3 -} - -public class Options -{ - public Options(GenerateRequest generateRequest) - { - var text = Encoding.UTF8.GetString(generateRequest.PluginOptions.ToByteArray()); - var rawOptions = JsonSerializer.Deserialize(text) ?? throw new InvalidOperationException(); - - Enum.TryParse(rawOptions.DriverName, true, out DriverName outDriverName); - DriverName = outDriverName; - GenerateGemfile = rawOptions.GenerateGemfile; - GenerateTypes = rawOptions.GenerateTypes; - RubyVersion = RubyVersionExtensions.ParseName(rawOptions.RubyVersion); - } - - public DriverName DriverName { get; } - - public RubyVersion RubyVersion { get; } - - public bool GenerateGemfile { get; } - - public bool GenerateTypes { get; } -} \ No newline at end of file diff --git a/PluginOptions/PluginOptions.csproj b/PluginOptions/PluginOptions.csproj deleted file mode 100644 index ecf010c..0000000 --- a/PluginOptions/PluginOptions.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - SqlcGenRuby - - - - - - - - - - - - diff --git a/PluginOptions/RawOptions.cs b/PluginOptions/RawOptions.cs deleted file mode 100644 index d3e6507..0000000 --- a/PluginOptions/RawOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; - -namespace SqlcGenRuby; - -internal class RawOptions -{ - [JsonPropertyName("driver")] - public required string DriverName { get; init; } - - [JsonPropertyName("generateGemfile")] - public bool GenerateGemfile { get; init; } // not generating Gemfile files by default - - [JsonPropertyName("generateTypes")] - public bool GenerateTypes { get; init; } = true; - - [JsonPropertyName("rubyVersion")] - public string RubyVersion { get; init; } = "3.3"; -} \ No newline at end of file diff --git a/PluginOptions/RubyVersion.cs b/PluginOptions/RubyVersion.cs deleted file mode 100644 index d11bbb8..0000000 --- a/PluginOptions/RubyVersion.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace SqlcGenRuby; - -public record RubyVersion(short Major, short? Minor, short? Patch = null) : IComparable -{ - public int CompareTo(RubyVersion? other) - { - if (other is null) return 1; - - var majorComparison = Major.CompareTo(other.Major); - if (majorComparison != 0) return majorComparison; - - if (Minor == null) - return majorComparison; - - var minorComparison = Minor.Value.CompareTo(other.Minor); - if (minorComparison != 0) return minorComparison; - - return Patch?.CompareTo(other.Patch) ?? minorComparison; - } - - public bool AtLeast(RubyVersion other) => CompareTo(other) >= 0; - - public bool AtMost(RubyVersion other) => CompareTo(other) <= 0; -} - -public static partial class RubyVersionExtensions -{ - private static readonly RubyVersion MinSupportedVersion = new(3, 1); - private static readonly RubyVersion MaxSupportedVersion = new(3, 3); - - public static RubyVersion ParseName(string versionPattern) - { - var rubyVersion = ParseSemanticVersion(versionPattern); - if (!rubyVersion.AtLeast(MinSupportedVersion)) - throw new ArgumentException($"Provided version {rubyVersion} exceeds min version {MinSupportedVersion}"); - if (!rubyVersion.AtMost(MaxSupportedVersion)) - throw new ArgumentException($"Provided version {rubyVersion} exceeds max version {MaxSupportedVersion}"); - return rubyVersion; - } - - private static RubyVersion ParseSemanticVersion(string versionPattern) - { - var semanticVersionRegex = SemanticVersionRegex(); - var match = semanticVersionRegex.Match(versionPattern); - if (!match.Success) - throw new ArgumentException($"version {versionPattern} can't be parsed to a semantic version"); - - var major = short.Parse(match.Groups[1].Value); - short? minor = null; - if (match.Groups[2].Success) - minor = short.Parse(match.Groups[2].Value); - short? patch = null; - if (match.Groups[3].Success) - patch = short.Parse(match.Groups[3].Value); - - return new RubyVersion(major, minor, patch); - } - - public static bool ImmutableDataSupported(this RubyVersion me) - { - return me.AtLeast(new RubyVersion(3, 2)); - } - - [GeneratedRegex(@"^\s*(0|[1-9][0-9]*)(?:\.(0|[1-9][0-9]*))?(?:\.(0|[1-9][0-9]*))?\s*$")] - private static partial Regex SemanticVersionRegex(); -} \ No newline at end of file diff --git a/Rakefile b/Rakefile index d583c95..90e1895 100755 --- a/Rakefile +++ b/Rakefile @@ -44,16 +44,3 @@ task :sqlc_generate_wasm => [:wasm_build, :update_wasm_plugin] do end task :test_wasm_plugin => [:test, :sqlc_generate_wasm, :update_wasm_plugin, :run_tests] - -# --- Legacy C# implementation (removed once the Ruby port is the only plugin) --- -task :protobuf_generate do - sh './scripts/generate_protobuf.sh' -end - -task :dotnet_format do - sh 'dotnet format --exclude GeneratedProtobuf' -end - -task :dotnet_publish_process => [:protobuf_generate, :dotnet_format] do - sh 'dotnet publish LocalRunner -c release --output dist/' -end diff --git a/RubySyntax/Basic.cs b/RubySyntax/Basic.cs deleted file mode 100644 index 0730003..0000000 --- a/RubySyntax/Basic.cs +++ /dev/null @@ -1,167 +0,0 @@ -using SqlcGenRuby; - -namespace RubyCodegen; - -public class RequireGem(string gem) -{ - public string Build() - { - return $"require '{gem}'"; - } - - public string Name() - { - return gem; - } -} - -public class ModuleDeclaration(string name, IEnumerable members) : IComposable, IRbsType -{ - public string BuildCode() - { - var moduleBody = members - .Select(m => m.BuildCode()) - .JoinByNewLine(2) - .Indent(); - return $"module {name}\n{moduleBody}\nend"; - } - - public string BuildType() - { - var moduleBody = members - .Where(m => m is IRbsType) - .Select(m => ((IRbsType)m).BuildType()) - .JoinByNewLine(2) - .Indent(); - return $"module {name}\n{moduleBody}\nend"; - } -} - -public class ClassDeclaration(string name, IEnumerable members) : IComposable, IRbsType -{ - public string BuildCode() - { - var classBody = members - .Select(m => m.BuildCode()) - .JoinByNewLine(2) - .Indent(); - return $"class {name}\n{classBody}\nend"; - } - - public string BuildType() - { - var moduleBody = members - .Where(m => m is IRbsType) - .Select(m => ((IRbsType)m).BuildType()) - .JoinByNewLine() - .Indent(); - return $"class {name}\n{moduleBody}\nend"; - } -} - -public class MethodDeclaration(string name, string? argInterface, string? args, string? returnInterface, - IEnumerable statements) : IComposable, IRbsType -{ - public string BuildCode() - { - var methodParams = args is null ? string.Empty : $"({args})"; - var methodBody = statements - .Select(c => c.BuildCode()) - .JoinByNewLine() - .Indent(); - return $"def {name}{methodParams}\n{methodBody}\nend"; - } - - public string BuildType() - { - var methodParams = args is null ? string.Empty : $"({argInterface}) "; - var returnType = returnInterface ?? "void"; - var propertiesDef = statements - .Where(s => s is PropertyDeclaration) - .Select(s => ((IRbsType)s).BuildType()) - .JoinByNewLine(); - return $"{propertiesDef}\ndef {name}: {methodParams}-> {returnType}"; - } -} - -public class PropertyDeclaration(string name, string constType, IComposable value) : IComposableRbsType -{ - public string BuildCode() - { - return $"{name.FirstCharToUpper()} = {value.BuildCode()}".TrimTrailingWhitespacesPerLine(); - } - - public string BuildType() - { - return $"{name}: {constType}"; - } -} - -public class SimpleStatement(string assignment, IComposable expression) : IComposable -{ - public string BuildCode() - { - return $"{assignment} = {expression.BuildCode()}".TrimTrailingWhitespacesPerLine(); - } -} - -public class SimpleExpression(string expression) : IComposable -{ - public string BuildCode() - { - return expression; - } -} - -public class DataDefine(string name, Dictionary nameToType) : IComposableRbsType -{ - public string BuildCode() - { - var attributes = nameToType - .Select(kv => $":{kv.Key}") - .ToList() - .JoinByCommaAndFormat(); - return $"class {name} < Data.define({attributes})\nend"; - } - - public string BuildType() - { - var attributes = nameToType - .Select(column => $"attr_reader {column.Key}: {column.Value}") - .ToList() - .JoinByNewLine() - .Indent(); - var initializeArgs = nameToType - .Select(kv => $"{kv.Value} {kv.Key}") - .ToList() - .JoinByCommaAndFormat() - .Indent(); - var initializeDef = $"def initialize: ({initializeArgs}) -> void".Indent(); - return $"class {name}\n{attributes}\n{initializeDef}\nend"; - } -} - -public class NewStruct(string name, Dictionary nameToType) : IComposableRbsType -{ - public string BuildCode() - { - var initializeDef = nameToType.Select(kv => kv.Key).ToList().JoinByCommaAndFormat(); - return $"class {name} < Struct.new({initializeDef})\nend"; - } - - public string BuildType() - { - var attributes = nameToType - .Select(kv => $"attr_accessor {kv.Key}: {kv.Value}") - .ToList() - .JoinByNewLine() - .Indent(); - var initializeArgs = nameToType - .Select(kv => $"{kv.Value} {kv.Key}") - .ToList() - .JoinByCommaAndFormat() - .Indent(); - var initializeDef = $"def self.new: ({initializeArgs}) -> {name}".Indent(); - return $"class {name} < ::Struct\n{attributes}\n{initializeDef}\nend"; - } -} \ No newline at end of file diff --git a/RubySyntax/Flows.cs b/RubySyntax/Flows.cs deleted file mode 100644 index 55c081a..0000000 --- a/RubySyntax/Flows.cs +++ /dev/null @@ -1,94 +0,0 @@ -using SqlcGenRuby; - -namespace RubyCodegen; - -public class WithResource(string resourceFrom, string resourceName, IList statements) : IComposable -{ - public string BuildCode() - { - var withResourceBody = statements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .TrimTrailingWhitespacesPerLine() - .Indent(); - var withResource = $"{resourceFrom}.with do |{resourceName}|\n{withResourceBody}\nend"; - return withResource; - } -} - -public class IfCondition(string condition, IList thenStatements, - IList? elseStatements = null) : IComposable -{ - public string BuildCode() - { - var thenBody = thenStatements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .TrimTrailingWhitespacesPerLine() - .Indent(); - if (elseStatements is null || elseStatements.Count == 0) - return $"if {condition}\n{thenBody}\nend"; - var elseBody = elseStatements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .TrimTrailingWhitespacesPerLine() - .Indent(); - return $"if {condition}\n{thenStatements}\nelse\n{elseBody}\nend"; - } -} - -public class UnlessCondition(string condition, IList thenStatements) : IComposable -{ - public string BuildCode() - { - var thenBody = thenStatements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .TrimTrailingWhitespacesPerLine() - .Indent(); - return $"unless {condition}\n{thenBody}\nend"; - } -} - -public class NewObject(string objectType, IList initExpressions, - IList? bodyStatements = null) : IComposable -{ - public string BuildCode() - { - var initParams = initExpressions - .Select(e => e.BuildCode()) - .ToList() - .JoinByCommaAndFormat(); - var baseCommand = $"{objectType}.new({initParams})"; - if (bodyStatements is null) - return baseCommand; - - var body = "{\n" + bodyStatements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .TrimTrailingWhitespacesPerLine() - .Indent() + "\n}"; - return $"{baseCommand} {body}"; - } -} - -public class ForeachLoop(string collectionVar, string controlVar, IList statements) : IComposable -{ - public string BuildCode() - { - var foreachBody = statements - .Select(s => s.BuildCode()) - .JoinByNewLine() - .Indent(); - var foreachLoop = $"{collectionVar}.each do |{controlVar}|\n{foreachBody}\nend"; - return foreachLoop; - } -} - -public class ListAppend(string listVar, NewObject newObject) : IComposable -{ - public string BuildCode() - { - return $"{listVar} << {newObject.BuildCode()}"; - } -} \ No newline at end of file diff --git a/RubySyntax/Interfaces.cs b/RubySyntax/Interfaces.cs deleted file mode 100644 index 618fbbf..0000000 --- a/RubySyntax/Interfaces.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace RubyCodegen; - -public interface IComposable -{ - string BuildCode(); -} - -public interface IRbsType -{ - string BuildType(); -} - -public interface IComposableRbsType : IComposable, IRbsType; \ No newline at end of file diff --git a/RubySyntax/RubySyntax.csproj b/RubySyntax/RubySyntax.csproj deleted file mode 100644 index 60d592b..0000000 --- a/RubySyntax/RubySyntax.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - RubyCodegen - - - - - - diff --git a/SqlcGenRuby/PluginRunner.cs b/SqlcGenRuby/PluginRunner.cs deleted file mode 100644 index 85c04c9..0000000 --- a/SqlcGenRuby/PluginRunner.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Google.Protobuf; -using Plugin; - -namespace SqlcGenRuby; - -public static class PluginRunner -{ - public static void Run() - { - var generateRequest = ReadInput(); - var generateResponse = new CodeGenerator().Generate(generateRequest); - WriteOutput(generateResponse.Result); - } - - private static GenerateRequest ReadInput() - { - using var memoryStream = new MemoryStream(); - Console.OpenStandardInput().CopyTo(memoryStream); - memoryStream.Position = 0; - var generateRequest = GenerateRequest.Parser.ParseFrom(memoryStream); - return generateRequest; - } - - private static void WriteOutput(GenerateResponse output) - { - var encodedOutput = output.ToByteArray(); - using var stdout = Console.OpenStandardOutput(); - stdout.Write(encodedOutput, 0, encodedOutput.Length); - } -} \ No newline at end of file diff --git a/SqlcGenRuby/SqlcGenRuby.csproj b/SqlcGenRuby/SqlcGenRuby.csproj deleted file mode 100644 index 9424614..0000000 --- a/SqlcGenRuby/SqlcGenRuby.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - diff --git a/WasmRunner/App.cs b/WasmRunner/App.cs deleted file mode 100644 index ed6c135..0000000 --- a/WasmRunner/App.cs +++ /dev/null @@ -1,3 +0,0 @@ -using SqlcGenRuby; - -PluginRunner.Run(); \ No newline at end of file diff --git a/WasmRunner/Properties/AssemblyInfo.cs b/WasmRunner/Properties/AssemblyInfo.cs deleted file mode 100644 index f1455cc..0000000 --- a/WasmRunner/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.Versioning; - -[assembly: SupportedOSPlatform("wasi")] \ No newline at end of file diff --git a/WasmRunner/WasmRunner.csproj b/WasmRunner/WasmRunner.csproj deleted file mode 100644 index 032e01e..0000000 --- a/WasmRunner/WasmRunner.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - net8.0 - SqlcGenRuby - Exe - wasi-wasm - true - - - true - true - false - true - false - false - - - - - - diff --git a/WasmRunner/runtimeconfig.template.json b/WasmRunner/runtimeconfig.template.json deleted file mode 100644 index 9ff7984..0000000 --- a/WasmRunner/runtimeconfig.template.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "wasmHostProperties": { - "perHostConfig": [ - { - "name": "wasmtime", - "Host": "wasmtime" - } - ] - } -} diff --git a/buf.gen.yaml b/buf.gen.yaml deleted file mode 100644 index d2517e2..0000000 --- a/buf.gen.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: v1 -plugins: - - plugin: buf.build/protocolbuffers/csharp - out: GeneratedProtobuf \ No newline at end of file diff --git a/scripts/generate_dockerfile.sh b/scripts/generate_dockerfile.sh deleted file mode 100755 index ec4b42c..0000000 --- a/scripts/generate_dockerfile.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -filename=$1 - -cat < "$filename" -# auto-generated by $0 - do not edit -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env - -WORKDIR /app -COPY *.sln . - -EOF - -projects=$(dotnet sln list | grep csproj | awk -F'/' '{print $1}') -for project in $projects; do - echo "COPY $project/*.csproj ./$project/" >> "$filename" -done -echo "" >> "$filename" -for project in $projects; do - echo "COPY $project/ ./$project/" >> "$filename" -done -echo "" >> "$filename" -echo "RUN dotnet restore" >> "$filename" \ No newline at end of file diff --git a/scripts/generate_protobuf.sh b/scripts/generate_protobuf.sh deleted file mode 100755 index 8767106..0000000 --- a/scripts/generate_protobuf.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -if wget -q --spider http://google.com; then - buf generate --template buf.gen.yaml buf.build/sqlc/sqlc --path plugin/ -else - echo "No internet connection - using pre-existing protobuf files.." -fi \ No newline at end of file diff --git a/scripts/wasm/copy_plugin_to.sh b/scripts/wasm/copy_plugin_to.sh deleted file mode 100755 index ea7591c..0000000 --- a/scripts/wasm/copy_plugin_to.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -source .env - -target_dir=$1 -if [ "${GITHUB_ACTIONS}" = "true" ]; then - WASM_FILE="${SOURCE_WASM_FILE_UBUNTU}" -else - WASM_FILE="${SOURCE_WASM_FILE}" -fi - -echo "WASM_FILE = ${WASM_FILE}" -mkdir -p "${target_dir}" && cp "${WASM_FILE}" "${target_dir}/plugin.wasm" -echo "WASM filesize:" && du -sh "${target_dir}/plugin.wasm" \ No newline at end of file diff --git a/sqlc-gen-ruby.sln b/sqlc-gen-ruby.sln deleted file mode 100644 index 385be9c..0000000 --- a/sqlc-gen-ruby.sln +++ /dev/null @@ -1,73 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalRunner", "LocalRunner\LocalRunner.csproj", "{649CFB46-68D1-41F8-BD76-FF4B79B16825}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions", "Extensions\Extensions.csproj", "{FCE3B072-0B96-47D2-959C-111B5BC1D292}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drivers", "Drivers\Drivers.csproj", "{6C698DAA-5151-4241-B07E-3435A717ABFB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WasmRunner", "WasmRunner\WasmRunner.csproj", "{D3095033-1C96-42D9-AE44-3D4F1B24CB51}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlcGenRuby", "SqlcGenRuby\SqlcGenRuby.csproj", "{B83D6813-3B06-4E23-B2FC-47078CB90B83}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratedProtobuf", "GeneratedProtobuf\GeneratedProtobuf.csproj", "{F9634820-EDE9-4DB0-9801-C32AD492D4E7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGenerator", "CodeGenerator\CodeGenerator.csproj", "{B0F7D8EC-BFFC-42E7-8B34-12CC34B863B0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginOptions", "PluginOptions\PluginOptions.csproj", "{ABD93C33-BEBE-4DE9-9ABF-430DAC1A938C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RubySyntax", "RubySyntax\RubySyntax.csproj", "{50DA9D94-29D1-42DE-86C2-D5719922ABB0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {649CFB46-68D1-41F8-BD76-FF4B79B16825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {649CFB46-68D1-41F8-BD76-FF4B79B16825}.Debug|Any CPU.Build.0 = Debug|Any CPU - {649CFB46-68D1-41F8-BD76-FF4B79B16825}.Release|Any CPU.ActiveCfg = Release|Any CPU - {649CFB46-68D1-41F8-BD76-FF4B79B16825}.Release|Any CPU.Build.0 = Release|Any CPU - {FCE3B072-0B96-47D2-959C-111B5BC1D292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCE3B072-0B96-47D2-959C-111B5BC1D292}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCE3B072-0B96-47D2-959C-111B5BC1D292}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCE3B072-0B96-47D2-959C-111B5BC1D292}.Release|Any CPU.Build.0 = Release|Any CPU - {6C698DAA-5151-4241-B07E-3435A717ABFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C698DAA-5151-4241-B07E-3435A717ABFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C698DAA-5151-4241-B07E-3435A717ABFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C698DAA-5151-4241-B07E-3435A717ABFB}.Release|Any CPU.Build.0 = Release|Any CPU - {3675F864-9CD2-49C0-9754-56E490F15DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3675F864-9CD2-49C0-9754-56E490F15DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3675F864-9CD2-49C0-9754-56E490F15DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3675F864-9CD2-49C0-9754-56E490F15DF0}.Release|Any CPU.Build.0 = Release|Any CPU - {D3095033-1C96-42D9-AE44-3D4F1B24CB51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3095033-1C96-42D9-AE44-3D4F1B24CB51}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3095033-1C96-42D9-AE44-3D4F1B24CB51}.Release|Any CPU.Build.0 = Release|Any CPU - {B83D6813-3B06-4E23-B2FC-47078CB90B83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B83D6813-3B06-4E23-B2FC-47078CB90B83}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B83D6813-3B06-4E23-B2FC-47078CB90B83}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B83D6813-3B06-4E23-B2FC-47078CB90B83}.Release|Any CPU.Build.0 = Release|Any CPU - {F9634820-EDE9-4DB0-9801-C32AD492D4E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F9634820-EDE9-4DB0-9801-C32AD492D4E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F9634820-EDE9-4DB0-9801-C32AD492D4E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F9634820-EDE9-4DB0-9801-C32AD492D4E7}.Release|Any CPU.Build.0 = Release|Any CPU - {B0F7D8EC-BFFC-42E7-8B34-12CC34B863B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B0F7D8EC-BFFC-42E7-8B34-12CC34B863B0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B0F7D8EC-BFFC-42E7-8B34-12CC34B863B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B0F7D8EC-BFFC-42E7-8B34-12CC34B863B0}.Release|Any CPU.Build.0 = Release|Any CPU - {ABD93C33-BEBE-4DE9-9ABF-430DAC1A938C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ABD93C33-BEBE-4DE9-9ABF-430DAC1A938C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ABD93C33-BEBE-4DE9-9ABF-430DAC1A938C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ABD93C33-BEBE-4DE9-9ABF-430DAC1A938C}.Release|Any CPU.Build.0 = Release|Any CPU - {50DA9D94-29D1-42DE-86C2-D5719922ABB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {50DA9D94-29D1-42DE-86C2-D5719922ABB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {50DA9D94-29D1-42DE-86C2-D5719922ABB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {50DA9D94-29D1-42DE-86C2-D5719922ABB0}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/sqlc-gen-ruby.sln.DotSettings b/sqlc-gen-ruby.sln.DotSettings deleted file mode 100644 index afe9db6..0000000 --- a/sqlc-gen-ruby.sln.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file From 308907c6735cd1116bedea102292e89d84592fe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 13:51:42 +0000 Subject: [PATCH 07/15] Add the sqlc features the C# plugin supports: execrows, execlastid, slice, embed, copyfrom, enums, models, transactions New generator features (all three drivers unless noted), with end-to-end tests that run the generated code against real PostgreSQL, MariaDB (MySQL) and SQLite: - :execrows, and :execlastid for PostgreSQL / SQLite (RETURNING) in addition to MySQL - sqlc.arg / sqlc.narg: nullable parameters and columns are typed `T?` in the RBS - sqlc.slice (MySQL, SQLite): IN-list expanded at runtime, empty list -> NULL - sqlc.embed: embedded tables become model attributes (`author`, `author2`, ...); rows read by position - :copyfrom: PG COPY FROM STDIN, MySQL LOAD DATA LOCAL INFILE (CSV), SQLite batched multi-row INSERT - models.rb/.rbs: one class per table, one module with constants per enum (enum columns are Strings) - `transaction { |tx| ... }` on the generated query class - array columns/params (PostgreSQL), `overrides:` option (Query:column / *:column -> type, notNull) - RBS: :many returns Array[Row], :execrows Integer, :execlastid the id type - close prepared statements in mysql2/sqlite3 (they leaked and made SQLite refuse to COMMIT) - PostgreSQL: unknown OIDs (uuid, enums, money) decode as Strings without gem warnings; queries accept Ruby arrays via type_map_for_queries; the old `?` -> `$n` rewrite is gone (sqlc already emits $n) Tests: captured fixtures now include the catalog, golden snapshots are Ruby output (regenerate with UPDATE_GOLDEN=1), new features_test.rb; tests/end2end_{pg,mysql2,sqlite3}.rb rewritten. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- examples/authors/mysql/query.sql | 69 +- examples/authors/mysql/schema.sql | 17 + examples/authors/postgresql/query.sql | 81 ++- examples/authors/postgresql/schema.sql | 32 +- examples/authors/sqlite/query.sql | 54 ++ examples/authors/sqlite/schema.sql | 9 + examples/mysql2/models.rb | 67 ++ examples/mysql2/models.rbs | 118 +++ examples/mysql2/query_sql.rb | 479 +++++++++++-- examples/mysql2/query_sql.rbs | 339 ++++++--- examples/pg/models.rb | 53 ++ examples/pg/models.rbs | 93 +++ examples/pg/query_sql.rb | 414 ++++++++++- examples/pg/query_sql.rbs | 331 +++++++-- examples/sqlite3/models.rb | 9 + examples/sqlite3/models.rbs | 17 + examples/sqlite3/query_sql.rb | 315 +++++++- examples/sqlite3/query_sql.rbs | 173 ++++- lib/sqlc_gen_ruby/catalog.rb | 70 ++ lib/sqlc_gen_ruby/code.rb | 23 + lib/sqlc_gen_ruby/drivers/base.rb | 65 +- lib/sqlc_gen_ruby/drivers/mysql2.rb | 84 ++- lib/sqlc_gen_ruby/drivers/pg.rb | 51 +- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 36 +- lib/sqlc_gen_ruby/generator.rb | 123 +++- lib/sqlc_gen_ruby/method_gen.rb | 135 +++- lib/sqlc_gen_ruby/options.rb | 33 +- lib/sqlc_gen_ruby/plugin_pb.rb | 30 +- lib/sqlc_gen_ruby/row_layout.rb | 60 ++ lib/sqlc_gen_ruby/type_resolver.rb | 32 + scripts/capture_request.rb | 21 +- test/codec_test.rb | 28 +- test/extended_golden_test.rb | 32 +- test/features_test.rb | 117 +++ test/fixtures/mysql2.bin | 145 +++- test/fixtures/mysql2_extended.bin | 133 ---- test/fixtures/pg.bin | Bin 3056 -> 10951 bytes test/fixtures/pg_extended.bin | 98 --- test/fixtures/sqlite3.bin | Bin 1297 -> 4576 bytes test/fixtures/sqlite3_extended.bin | 59 -- test/golden/mysql2/default/models.rb | 67 ++ test/golden/mysql2/default/models.rbs | 118 +++ .../default}/my_queries_sql.rb | 21 +- .../default/my_queries_sql.rbs | 2 + test/golden/mysql2/default/other_sql.rb | 98 +++ .../default/other_sql.rbs | 11 + test/golden/mysql2/default/query_sql.rb | 669 +++++++++++++++++ test/golden/mysql2/default/query_sql.rbs | 390 ++++++++++ .../ruby31_gemfile/Gemfile | 3 +- test/golden/mysql2/ruby31_gemfile/models.rb | 67 ++ test/golden/mysql2/ruby31_gemfile/models.rbs | 118 +++ .../ruby31_gemfile/my_queries_sql.rb | 21 +- .../ruby31_gemfile/my_queries_sql.rbs | 2 + .../golden/mysql2/ruby31_gemfile/other_sql.rb | 98 +++ .../ruby31_gemfile/other_sql.rbs | 11 + .../golden/mysql2/ruby31_gemfile/query_sql.rb | 669 +++++++++++++++++ .../mysql2/ruby31_gemfile/query_sql.rbs | 390 ++++++++++ test/golden/mysql2/ruby32_notypes/models.rb | 67 ++ .../ruby32_notypes}/my_queries_sql.rb | 21 +- .../golden/mysql2/ruby32_notypes/other_sql.rb | 98 +++ .../golden/mysql2/ruby32_notypes/query_sql.rb | 669 +++++++++++++++++ .../mysql2_extended/default/options.json | 2 - .../mysql2_extended/default/other_sql.rb | 56 -- .../mysql2_extended/default/query_sql.rb | 294 -------- .../mysql2_extended/default/query_sql.rbs | 217 ------ .../ruby31_gemfile/options.json | 4 - .../ruby31_gemfile/other_sql.rb | 56 -- .../ruby31_gemfile/query_sql.rb | 294 -------- .../ruby31_gemfile/query_sql.rbs | 217 ------ .../ruby32_notypes/options.json | 4 - .../ruby32_notypes/other_sql.rb | 56 -- .../ruby32_notypes/query_sql.rb | 294 -------- test/golden/pg/default/models.rb | 53 ++ test/golden/pg/default/models.rbs | 93 +++ .../default}/my_queries_sql.rb | 18 +- .../default/my_queries_sql.rbs | 2 + .../{pg_extended => pg}/default/other_sql.rb | 40 +- .../{pg_extended => pg}/default/other_sql.rbs | 11 + test/golden/pg/default/query_sql.rb | 671 ++++++++++++++++++ test/golden/pg/default/query_sql.rbs | 408 +++++++++++ .../ruby31_gemfile/Gemfile | 0 test/golden/pg/ruby31_gemfile/models.rb | 53 ++ test/golden/pg/ruby31_gemfile/models.rbs | 93 +++ .../ruby31_gemfile/my_queries_sql.rb | 18 +- .../ruby31_gemfile/my_queries_sql.rbs | 2 + .../ruby31_gemfile/other_sql.rb | 40 +- .../ruby31_gemfile/other_sql.rbs | 11 + test/golden/pg/ruby31_gemfile/query_sql.rb | 671 ++++++++++++++++++ test/golden/pg/ruby31_gemfile/query_sql.rbs | 408 +++++++++++ test/golden/pg/ruby32_notypes/models.rb | 53 ++ .../ruby32_notypes}/my_queries_sql.rb | 18 +- .../ruby32_notypes/other_sql.rb | 40 +- test/golden/pg/ruby32_notypes/query_sql.rb | 671 ++++++++++++++++++ test/golden/pg_extended/default/options.json | 2 - test/golden/pg_extended/default/query_sql.rb | 259 ------- test/golden/pg_extended/default/query_sql.rbs | 167 ----- .../pg_extended/ruby31_gemfile/options.json | 4 - .../pg_extended/ruby31_gemfile/query_sql.rb | 259 ------- .../pg_extended/ruby31_gemfile/query_sql.rbs | 167 ----- .../pg_extended/ruby32_notypes/options.json | 4 - .../pg_extended/ruby32_notypes/query_sql.rb | 259 ------- test/golden/sqlite3/default/models.rb | 9 + test/golden/sqlite3/default/models.rbs | 17 + .../default/my_queries_sql.rb | 13 +- .../default/my_queries_sql.rbs | 2 + .../default}/other_sql.rb | 45 +- .../default/other_sql.rbs | 11 + test/golden/sqlite3/default/query_sql.rb | 462 ++++++++++++ test/golden/sqlite3/default/query_sql.rbs | 273 +++++++ .../ruby31_gemfile/Gemfile | 0 test/golden/sqlite3/ruby31_gemfile/models.rb | 9 + test/golden/sqlite3/ruby31_gemfile/models.rbs | 17 + .../ruby31_gemfile/my_queries_sql.rb | 13 +- .../ruby31_gemfile/my_queries_sql.rbs | 2 + .../ruby31_gemfile/other_sql.rb | 45 +- .../ruby31_gemfile/other_sql.rbs | 11 + .../sqlite3/ruby31_gemfile/query_sql.rb | 462 ++++++++++++ .../sqlite3/ruby31_gemfile/query_sql.rbs | 273 +++++++ test/golden/sqlite3/ruby32_notypes/models.rb | 9 + .../ruby32_notypes/my_queries_sql.rb | 13 +- .../ruby32_notypes}/other_sql.rb | 45 +- .../sqlite3/ruby32_notypes/query_sql.rb | 462 ++++++++++++ .../sqlite3_extended/default/options.json | 2 - .../sqlite3_extended/default/query_sql.rb | 159 ----- .../sqlite3_extended/default/query_sql.rbs | 114 --- .../ruby31_gemfile/options.json | 4 - .../ruby31_gemfile/query_sql.rb | 159 ----- .../ruby31_gemfile/query_sql.rbs | 114 --- .../ruby32_notypes/options.json | 4 - .../ruby32_notypes/query_sql.rb | 159 ----- test/golden_test.rb | 19 +- test/synthetic_requests.rb | 44 ++ test/test_helper.rb | 1 + tests/consts.rb | 2 +- tests/end2end_mysql2.rb | 265 +++++-- tests/end2end_pg.rb | 288 ++++++-- tests/end2end_sqlite3.rb | 221 ++++-- 137 files changed, 13196 insertions(+), 4196 deletions(-) create mode 100644 examples/mysql2/models.rb create mode 100644 examples/mysql2/models.rbs create mode 100644 examples/pg/models.rb create mode 100644 examples/pg/models.rbs create mode 100644 examples/sqlite3/models.rb create mode 100644 examples/sqlite3/models.rbs create mode 100644 lib/sqlc_gen_ruby/catalog.rb create mode 100644 lib/sqlc_gen_ruby/row_layout.rb create mode 100644 lib/sqlc_gen_ruby/type_resolver.rb create mode 100644 test/features_test.rb delete mode 100644 test/fixtures/mysql2_extended.bin delete mode 100644 test/fixtures/pg_extended.bin delete mode 100644 test/fixtures/sqlite3_extended.bin create mode 100644 test/golden/mysql2/default/models.rb create mode 100644 test/golden/mysql2/default/models.rbs rename test/golden/{mysql2_extended/ruby32_notypes => mysql2/default}/my_queries_sql.rb (63%) rename test/golden/{mysql2_extended => mysql2}/default/my_queries_sql.rbs (87%) create mode 100644 test/golden/mysql2/default/other_sql.rb rename test/golden/{mysql2_extended => mysql2}/default/other_sql.rbs (75%) create mode 100644 test/golden/mysql2/default/query_sql.rb create mode 100644 test/golden/mysql2/default/query_sql.rbs rename test/golden/{mysql2_extended => mysql2}/ruby31_gemfile/Gemfile (70%) create mode 100644 test/golden/mysql2/ruby31_gemfile/models.rb create mode 100644 test/golden/mysql2/ruby31_gemfile/models.rbs rename test/golden/{mysql2_extended => mysql2}/ruby31_gemfile/my_queries_sql.rb (63%) rename test/golden/{mysql2_extended => mysql2}/ruby31_gemfile/my_queries_sql.rbs (88%) create mode 100644 test/golden/mysql2/ruby31_gemfile/other_sql.rb rename test/golden/{mysql2_extended => mysql2}/ruby31_gemfile/other_sql.rbs (74%) create mode 100644 test/golden/mysql2/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/mysql2/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/mysql2/ruby32_notypes/models.rb rename test/golden/{mysql2_extended/default => mysql2/ruby32_notypes}/my_queries_sql.rb (63%) create mode 100644 test/golden/mysql2/ruby32_notypes/other_sql.rb create mode 100644 test/golden/mysql2/ruby32_notypes/query_sql.rb delete mode 100644 test/golden/mysql2_extended/default/options.json delete mode 100644 test/golden/mysql2_extended/default/other_sql.rb delete mode 100644 test/golden/mysql2_extended/default/query_sql.rb delete mode 100644 test/golden/mysql2_extended/default/query_sql.rbs delete mode 100644 test/golden/mysql2_extended/ruby31_gemfile/options.json delete mode 100644 test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb delete mode 100644 test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb delete mode 100644 test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs delete mode 100644 test/golden/mysql2_extended/ruby32_notypes/options.json delete mode 100644 test/golden/mysql2_extended/ruby32_notypes/other_sql.rb delete mode 100644 test/golden/mysql2_extended/ruby32_notypes/query_sql.rb create mode 100644 test/golden/pg/default/models.rb create mode 100644 test/golden/pg/default/models.rbs rename test/golden/{pg_extended/ruby32_notypes => pg/default}/my_queries_sql.rb (62%) rename test/golden/{pg_extended => pg}/default/my_queries_sql.rbs (88%) rename test/golden/{pg_extended => pg}/default/other_sql.rb (56%) rename test/golden/{pg_extended => pg}/default/other_sql.rbs (76%) create mode 100644 test/golden/pg/default/query_sql.rb create mode 100644 test/golden/pg/default/query_sql.rbs rename test/golden/{pg_extended => pg}/ruby31_gemfile/Gemfile (100%) create mode 100644 test/golden/pg/ruby31_gemfile/models.rb create mode 100644 test/golden/pg/ruby31_gemfile/models.rbs rename test/golden/{pg_extended => pg}/ruby31_gemfile/my_queries_sql.rb (62%) rename test/golden/{pg_extended => pg}/ruby31_gemfile/my_queries_sql.rbs (89%) rename test/golden/{pg_extended => pg}/ruby31_gemfile/other_sql.rb (55%) rename test/golden/{pg_extended => pg}/ruby31_gemfile/other_sql.rbs (75%) create mode 100644 test/golden/pg/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/pg/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/pg/ruby32_notypes/models.rb rename test/golden/{pg_extended/default => pg/ruby32_notypes}/my_queries_sql.rb (62%) rename test/golden/{pg_extended => pg}/ruby32_notypes/other_sql.rb (56%) create mode 100644 test/golden/pg/ruby32_notypes/query_sql.rb delete mode 100644 test/golden/pg_extended/default/options.json delete mode 100644 test/golden/pg_extended/default/query_sql.rb delete mode 100644 test/golden/pg_extended/default/query_sql.rbs delete mode 100644 test/golden/pg_extended/ruby31_gemfile/options.json delete mode 100644 test/golden/pg_extended/ruby31_gemfile/query_sql.rb delete mode 100644 test/golden/pg_extended/ruby31_gemfile/query_sql.rbs delete mode 100644 test/golden/pg_extended/ruby32_notypes/options.json delete mode 100644 test/golden/pg_extended/ruby32_notypes/query_sql.rb create mode 100644 test/golden/sqlite3/default/models.rb create mode 100644 test/golden/sqlite3/default/models.rbs rename test/golden/{sqlite3_extended => sqlite3}/default/my_queries_sql.rb (72%) rename test/golden/{sqlite3_extended => sqlite3}/default/my_queries_sql.rbs (86%) rename test/golden/{sqlite3_extended/ruby32_notypes => sqlite3/default}/other_sql.rb (50%) rename test/golden/{sqlite3_extended => sqlite3}/default/other_sql.rbs (74%) create mode 100644 test/golden/sqlite3/default/query_sql.rb create mode 100644 test/golden/sqlite3/default/query_sql.rbs rename test/golden/{sqlite3_extended => sqlite3}/ruby31_gemfile/Gemfile (100%) create mode 100644 test/golden/sqlite3/ruby31_gemfile/models.rb create mode 100644 test/golden/sqlite3/ruby31_gemfile/models.rbs rename test/golden/{sqlite3_extended => sqlite3}/ruby31_gemfile/my_queries_sql.rb (72%) rename test/golden/{sqlite3_extended => sqlite3}/ruby31_gemfile/my_queries_sql.rbs (87%) rename test/golden/{sqlite3_extended => sqlite3}/ruby31_gemfile/other_sql.rb (50%) rename test/golden/{sqlite3_extended => sqlite3}/ruby31_gemfile/other_sql.rbs (73%) create mode 100644 test/golden/sqlite3/ruby31_gemfile/query_sql.rb create mode 100644 test/golden/sqlite3/ruby31_gemfile/query_sql.rbs create mode 100644 test/golden/sqlite3/ruby32_notypes/models.rb rename test/golden/{sqlite3_extended => sqlite3}/ruby32_notypes/my_queries_sql.rb (72%) rename test/golden/{sqlite3_extended/default => sqlite3/ruby32_notypes}/other_sql.rb (50%) create mode 100644 test/golden/sqlite3/ruby32_notypes/query_sql.rb delete mode 100644 test/golden/sqlite3_extended/default/options.json delete mode 100644 test/golden/sqlite3_extended/default/query_sql.rb delete mode 100644 test/golden/sqlite3_extended/default/query_sql.rbs delete mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/options.json delete mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb delete mode 100644 test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs delete mode 100644 test/golden/sqlite3_extended/ruby32_notypes/options.json delete mode 100644 test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb create mode 100644 test/synthetic_requests.rb diff --git a/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index 5c6b9fd..0107730 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -1,34 +1,87 @@ -/* name: GetAuthor :one */ +-- name: GetAuthor :one SELECT * FROM authors WHERE id = ? LIMIT 1; -/* name: ListAuthors :many */ +-- name: ListAuthors :many SELECT * FROM authors ORDER BY name; -/* name: CreateAuthor :exec */ +-- name: CreateAuthor :exec INSERT INTO authors ( name, bio ) VALUES ( ?, ? ); -/* name: UpdateAuthor :exec */ +-- name: UpdateAuthor :exec UPDATE authors SET bio = ? WHERE id = ?; -/* name: CreateAuthorReturnId :execlastid */ +-- name: CreateAuthorReturnId :execlastid INSERT INTO authors ( name, bio ) VALUES ( ?, ? ); -/* name: DeleteAuthor :exec */ +-- name: DeleteAuthor :exec DELETE FROM authors WHERE id = ?; -/* name: Test :one */ +-- name: Test :one SELECT * FROM node_mysql_types -LIMIT 1; \ No newline at end of file +LIMIT 1; + +-- name: ListAuthorsPaged :many +SELECT * FROM authors +ORDER BY name +LIMIT ? OFFSET ?; + +-- name: GetAuthorByNamePattern :many +SELECT * FROM authors +WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); + +-- name: UpdateAuthorBios :execrows +UPDATE authors +SET bio = sqlc.arg('bio') +WHERE bio IS NOT NULL; + +-- name: GetAuthorsByIds :many +SELECT * FROM authors WHERE id IN (sqlc.slice('ids')); + +-- name: GetAuthorsByIdsAndNames :many +SELECT * FROM authors WHERE id IN (sqlc.slice('ids')) AND name IN (sqlc.slice('names')); + +-- name: CopyAuthors :copyfrom +INSERT INTO authors (name, bio) VALUES (?, ?); + +-- name: TruncateAuthors :exec +DELETE FROM authors; + +-- name: CreateBook :execlastid +INSERT INTO books (name, author_id) VALUES (?, ?); + +-- name: ListAllAuthorsBooks :many +SELECT sqlc.embed(authors), sqlc.embed(books) +FROM authors JOIN books ON authors.id = books.author_id +ORDER BY authors.name; + +-- name: GetDuplicateAuthors :many +SELECT sqlc.embed(authors1), sqlc.embed(authors2) +FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name +WHERE authors1.id < authors2.id; + +-- name: GetAuthorsByBookName :many +SELECT authors.*, sqlc.embed(books) +FROM authors JOIN books ON authors.id = books.author_id +WHERE books.name = ?; + +-- name: CreateExtendedBio :exec +INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?); + +-- name: GetFirstExtendedBioByType :one +SELECT * FROM extended_bios WHERE bio_type = ? LIMIT 1; + +-- name: TruncateExtendedBios :exec +DELETE FROM extended_bios; diff --git a/examples/authors/mysql/schema.sql b/examples/authors/mysql/schema.sql index 4f71ed3..58a8c07 100644 --- a/examples/authors/mysql/schema.sql +++ b/examples/authors/mysql/schema.sql @@ -46,3 +46,20 @@ CREATE TABLE node_mysql_types ( c_json JSON ); + + +CREATE TABLE books ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + name TEXT NOT NULL, + author_id BIGINT NOT NULL, + description TEXT, + FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE +); + +CREATE TABLE extended_bios ( + author_name VARCHAR(100), + name VARCHAR(100), + bio_type ENUM('Autobiography', 'Biography', 'Memoir'), + author_type SET('Author', 'Editor', 'Translator'), + PRIMARY KEY (author_name, name) +); diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index da3f365..27ef314 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -18,6 +18,83 @@ RETURNING *; DELETE FROM authors WHERE id = $1; -/* name: Test :one */ +-- name: Test :one SELECT * FROM node_postgres_types -LIMIT 1; \ No newline at end of file +LIMIT 1; + +-- name: ListAuthorsPaged :many +SELECT * FROM authors +ORDER BY name +LIMIT sqlc.arg('limit') +OFFSET sqlc.arg('offset'); + +-- name: GetAuthorByNamePattern :many +SELECT * FROM authors +WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); + +-- name: CreateAuthorReturnId :execlastid +INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id; + +-- name: UpdateAuthorBios :execrows +UPDATE authors +SET bio = $1 +WHERE bio IS NOT NULL; + +-- name: GetAuthorsByIds :many +SELECT * FROM authors +WHERE id = ANY($1::BIGINT []); + +-- name: GetAuthorsByIdsAndNames :many +SELECT * FROM authors +WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []); + +-- name: CopyAuthors :copyfrom +INSERT INTO authors (name, bio) VALUES ($1, $2); + +-- name: TruncateAuthors :exec +TRUNCATE TABLE authors CASCADE; + +-- name: CreateBook :execlastid +INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id; + +-- name: ListAllAuthorsBooks :many +SELECT + sqlc.embed(authors), + sqlc.embed(books) +FROM authors +INNER JOIN books ON authors.id = books.author_id +ORDER BY authors.name; + +-- name: GetDuplicateAuthors :many +SELECT + sqlc.embed(authors1), + sqlc.embed(authors2) +FROM authors AS authors1 +INNER JOIN authors AS authors2 ON authors1.name = authors2.name +WHERE authors1.id < authors2.id; + +-- name: GetAuthorsByBookName :many +SELECT + authors.*, + sqlc.embed(books) +FROM authors INNER JOIN books ON authors.id = books.author_id +WHERE books.name = $1; + +-- name: CreateExtendedBio :exec +INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3); + +-- name: GetFirstExtendedBioByType :one +SELECT * FROM extended.bios WHERE bio_type = $1 LIMIT 1; + +-- name: TruncateExtendedBios :exec +TRUNCATE TABLE extended.bios; + +-- name: InsertExtraTypes :exec +INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8); + +-- name: GetExtraTypes :one +SELECT * FROM postgres_extra_types LIMIT 1; + +-- name: TruncateExtraTypes :exec +TRUNCATE TABLE postgres_extra_types; diff --git a/examples/authors/postgresql/schema.sql b/examples/authors/postgresql/schema.sql index a17bf4d..6540944 100644 --- a/examples/authors/postgresql/schema.sql +++ b/examples/authors/postgresql/schema.sql @@ -27,4 +27,34 @@ CREATE TABLE node_postgres_types ( c_bytea BYTEA, c_text TEXT, c_json JSON -); \ No newline at end of file +); + +CREATE TABLE books ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + author_id BIGINT NOT NULL, + description TEXT, + FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE +); + +CREATE SCHEMA extended; + +CREATE TYPE extended.bio_type AS ENUM ('Autobiography', 'Biography', 'Memoir'); + +CREATE TABLE extended.bios ( + author_name VARCHAR(100), + name VARCHAR(100), + bio_type extended.bio_type, + PRIMARY KEY (author_name, name) +); + +CREATE TABLE postgres_extra_types ( + c_uuid UUID, + c_jsonb JSONB, + c_text_array TEXT [], + c_integer_array INTEGER [], + c_timestamptz TIMESTAMP WITH TIME ZONE, + c_inet INET, + c_money MONEY, + c_not_null TEXT NOT NULL DEFAULT 'x' +); diff --git a/examples/authors/sqlite/query.sql b/examples/authors/sqlite/query.sql index 25af781..c33ec38 100644 --- a/examples/authors/sqlite/query.sql +++ b/examples/authors/sqlite/query.sql @@ -16,3 +16,57 @@ INSERT INTO authors ( -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = ?; + +-- name: ListAuthorsPaged :many +SELECT * FROM authors +ORDER BY name +LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset'); + +-- name: GetAuthorByNamePattern :many +SELECT * FROM authors +WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); + +-- name: CreateAuthorReturnId :execlastid +INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id; + +-- name: UpdateAuthorBios :execrows +UPDATE authors +SET bio = ? +WHERE bio IS NOT NULL; + +-- name: GetAuthorsByIds :many +SELECT * FROM authors WHERE id IN (sqlc.slice('ids')); + +-- name: GetAuthorsByIdsAndNames :many +SELECT * FROM authors WHERE id IN (sqlc.slice('ids')) AND name IN (sqlc.slice('names')); + +-- name: CopyAuthors :copyfrom +INSERT INTO authors (name, bio) VALUES (?, ?); + +-- name: TruncateAuthors :exec +DELETE FROM authors; + +-- name: CreateBook :execlastid +INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id; + +-- name: ListAllAuthorsBooks :many +SELECT + sqlc.embed(authors), + sqlc.embed(books) +FROM authors INNER JOIN books ON authors.id = books.author_id +ORDER BY authors.name; + +-- name: GetDuplicateAuthors :many +SELECT + sqlc.embed(authors1), + sqlc.embed(authors2) +FROM authors AS authors1 +INNER JOIN authors AS authors2 ON authors1.name = authors2.name +WHERE authors1.id < authors2.id; + +-- name: GetAuthorsByBookName :many +SELECT + authors.*, + sqlc.embed(books) +FROM authors INNER JOIN books ON authors.id = books.author_id +WHERE books.name = ?; diff --git a/examples/authors/sqlite/schema.sql b/examples/authors/sqlite/schema.sql index 2a44974..549d6d4 100644 --- a/examples/authors/sqlite/schema.sql +++ b/examples/authors/sqlite/schema.sql @@ -3,3 +3,12 @@ CREATE TABLE authors ( name text NOT NULL, bio text ); + + +CREATE TABLE books ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + author_id INTEGER NOT NULL, + description TEXT, + FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE +); diff --git a/examples/mysql2/models.rb b/examples/mysql2/models.rb new file mode 100644 index 0000000..627f01b --- /dev/null +++ b/examples/mysql2/models.rb @@ -0,0 +1,67 @@ +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + module ExtendedBiosBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = "Author" + EDITOR = "Editor" + TRANSLATOR = "Translator" + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodeMysqlType < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) + end +end \ No newline at end of file diff --git a/examples/mysql2/models.rbs b/examples/mysql2/models.rbs new file mode 100644 index 0000000..10c20c0 --- /dev/null +++ b/examples/mysql2/models.rbs @@ -0,0 +1,118 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + module ExtendedBiosBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + module ExtendedBiosAuthorType + AUTHOR: String + EDITOR: String + TRANSLATOR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodeMysqlType + attr_reader c_bit: Array[Integer]? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: String? + attr_reader c_dec: String? + attr_reader c_numeric: String? + attr_reader c_fixed: String? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_datetime: String? + attr_reader c_timestamp: String? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: Array[Integer]? + attr_reader c_varbinary: Array[Integer]? + attr_reader c_tinyblob: Array[Integer]? + attr_reader c_tinytext: String? + attr_reader c_blob: Array[Integer]? + attr_reader c_text: String? + attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumtext: String? + attr_reader c_longblob: Array[Integer]? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end + + class ExtendedBio + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end +end \ No newline at end of file diff --git a/examples/mysql2/query_sql.rb b/examples/mysql2/query_sql.rb index f3d6626..dc89ba5 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -1,6 +1,9 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require 'csv' +require 'tempfile' +require_relative 'models' module Mysql2Codegen GetAuthorSql = %q(SELECT id, name, bio FROM authors @@ -93,6 +96,97 @@ class TestRow < Data.define( ) end + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ?) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:limit, :offset) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) + class QuerySql def initialize(connection_pool_params, mysql2_params) @db = ConnectionPool.new(**connection_pool_params) { @@ -100,27 +194,49 @@ def initialize(connection_pool_params, mysql2_params) } end + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + ensure + stmt.close + end end end def list_authors @db.with do |client| stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close end - return entities end end @@ -128,7 +244,11 @@ def create_author(create_author_args) @db.with do |client| query_params = [create_author_args.name, create_author_args.bio] stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end @@ -136,7 +256,11 @@ def update_author(update_author_args) @db.with do |client| query_params = [update_author_args.bio, update_author_args.id] stmt = client.prepare(UpdateAuthorSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end @@ -144,8 +268,12 @@ def create_author_return_id(create_author_return_id_args) @db.with do |client| query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] stmt = client.prepare(CreateAuthorReturnIdSql) - stmt.execute(*query_params) - return client.last_id + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end end end @@ -153,57 +281,292 @@ def delete_author(delete_author_args) @db.with do |client| query_params = [delete_author_args.id] stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end def test @db.with do |client| stmt = client.prepare(TestSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + ensure + stmt.close + end + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end end end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + return if copy_authors_args.empty? + Tempfile.create(['copy_authors', '.csv']) do |file| + copy_authors_args.each do |item| + file.write(csv_line([item.name, item.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + end + end + + def truncate_authors + @db.with do |client| + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + @db.with do |client| + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def get_duplicate_authors + @db.with do |client| + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params, as: :array) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] + stmt = client.prepare(CreateExtendedBioSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + stmt = client.prepare(GetFirstExtendedBioByTypeSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + return entity + ensure + stmt.close + end + end + end + + def truncate_extended_bios + @db.with do |client| + stmt = client.prepare(TruncateExtendedBiosSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + private + + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end end end \ No newline at end of file diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index 1bf0841..d8a5904 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -5,8 +5,8 @@ module Mysql2Codegen class GetAuthorRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end class GetAuthorArgs @@ -19,32 +19,32 @@ module Mysql2Codegen class ListAuthorsRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end CreateAuthorSql: String class CreateAuthorArgs attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void end UpdateAuthorSql: String class UpdateAuthorArgs - attr_reader bio: String + attr_reader bio: String? attr_reader id: Integer - def initialize: ( String bio, Integer id) -> void + def initialize: ( String? bio, Integer id) -> void end CreateAuthorReturnIdSql: String class CreateAuthorReturnIdArgs attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void end DeleteAuthorSql: String @@ -57,91 +57,232 @@ module Mysql2Codegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer] - attr_reader c_tinyint: Integer - attr_reader c_bool: Integer - attr_reader c_boolean: Integer - attr_reader c_smallint: Integer - attr_reader c_mediumint: Integer - attr_reader c_int: Integer - attr_reader c_integer: Integer - attr_reader c_bigint: Integer + attr_reader c_bit: Array[Integer]? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? attr_reader c_serial: Integer - attr_reader c_decimal: String - attr_reader c_dec: String - attr_reader c_numeric: String - attr_reader c_fixed: String - attr_reader c_float: Float - attr_reader c_double: Float - attr_reader c_double_precision: Float - attr_reader c_date: String - attr_reader c_time: String - attr_reader c_datetime: String - attr_reader c_timestamp: String - attr_reader c_year: Integer - attr_reader c_char: String - attr_reader c_nchar: String - attr_reader c_national_char: String - attr_reader c_varchar: String - attr_reader c_binary: Array[Integer] - attr_reader c_varbinary: Array[Integer] - attr_reader c_tinyblob: Array[Integer] - attr_reader c_tinytext: String - attr_reader c_blob: Array[Integer] - attr_reader c_text: String - attr_reader c_mediumblob: Array[Integer] - attr_reader c_mediumtext: String - attr_reader c_longblob: Array[Integer] - attr_reader c_longtext: String - attr_reader c_json: String + attr_reader c_decimal: String? + attr_reader c_dec: String? + attr_reader c_numeric: String? + attr_reader c_fixed: String? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_datetime: String? + attr_reader c_timestamp: String? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: Array[Integer]? + attr_reader c_varbinary: Array[Integer]? + attr_reader c_tinyblob: Array[Integer]? + attr_reader c_tinytext: String? + attr_reader c_blob: Array[Integer]? + attr_reader c_text: String? + attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumtext: String? + attr_reader c_longblob: Array[Integer]? + attr_reader c_longtext: String? + attr_reader c_json: String? def initialize: ( - Array[Integer] c_bit, - Integer c_tinyint, - Integer c_bool, - Integer c_boolean, - Integer c_smallint, - Integer c_mediumint, - Integer c_int, - Integer c_integer, - Integer c_bigint, + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, Integer c_serial, - String c_decimal, - String c_dec, - String c_numeric, - String c_fixed, - Float c_float, - Float c_double, - Float c_double_precision, - String c_date, - String c_time, - String c_datetime, - String c_timestamp, - Integer c_year, - String c_char, - String c_nchar, - String c_national_char, - String c_varchar, - Array[Integer] c_binary, - Array[Integer] c_varbinary, - Array[Integer] c_tinyblob, - String c_tinytext, - Array[Integer] c_blob, - String c_text, - Array[Integer] c_mediumblob, - String c_mediumtext, - Array[Integer] c_longblob, - String c_longtext, - String c_json + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json ) -> void end + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader limit: Integer + attr_reader offset: Integer + def initialize: ( Integer limit, Integer offset) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader ids: Array[Integer] + def initialize: ( Array[Integer] ids) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader ids: Array[Integer] + attr_reader names: Array[String] + def initialize: ( Array[Integer] ids, Array[String] names) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + class GetFirstExtendedBioByTypeArgs + attr_reader bio_type: String? + def initialize: ( String? bio_type) -> void + end + + TruncateExtendedBiosSql: String + class QuerySql @db: untyped def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def get_author: (GetAuthorArgs) -> GetAuthorRow? - def list_authors: -> void + def list_authors: -> Array[ListAuthorsRow] def create_author: (CreateAuthorArgs) -> void @@ -152,5 +293,37 @@ module Mysql2Codegen def delete_author: (DeleteAuthorArgs) -> void def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + private + def csv_line: (Array[untyped]) -> String + def slice_placeholders: (Array[untyped]) -> String end end \ No newline at end of file diff --git a/examples/pg/models.rb b/examples/pg/models.rb new file mode 100644 index 0000000..9539cb7 --- /dev/null +++ b/examples/pg/models.rb @@ -0,0 +1,53 @@ +# auto-generated by sqlc - do not edit + +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end \ No newline at end of file diff --git a/examples/pg/models.rbs b/examples/pg/models.rbs new file mode 100644 index 0000000..e7920f4 --- /dev/null +++ b/examples/pg/models.rbs @@ -0,0 +1,93 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodePostgresType + attr_reader c_bit: Array[Integer]? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: Float? + attr_reader c_numeric: Float? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_timestamp: String? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: Array[Integer]? + attr_reader c_text: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json + ) -> void + end + + class Book + attr_reader id: String + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( String id, String name, Integer author_id, String? description) -> void + end + + class PostgresExtraType + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + class ExtendedBio + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end +end \ No newline at end of file diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index dab0710..424f21b 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -2,6 +2,7 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen GetAuthorSql = %q(SELECT id, name, bio FROM authors @@ -63,16 +64,171 @@ class TestRow < Data.define( ) end + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT [])) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:param1) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) + + InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) + + class InsertExtraTypesArgs < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) + class QuerySql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] @@ -161,5 +317,261 @@ def test return entity end end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + unless @prepared_statements.include?('list_authors_paged') + client.prepare('list_authors_paged', ListAuthorsPagedSql) + @prepared_statements.add('list_authors_paged') + end + result = client.exec_prepared('list_authors_paged', query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + unless @prepared_statements.include?('get_author_by_name_pattern') + client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) + @prepared_statements.add('get_author_by_name_pattern') + end + result = client.exec_prepared('get_author_by_name_pattern', query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + unless @prepared_statements.include?('create_author_return_id') + client.prepare('create_author_return_id', CreateAuthorReturnIdSql) + @prepared_statements.add('create_author_return_id') + end + result = client.exec_prepared('create_author_return_id', query_params) + return result.first['id'] + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + unless @prepared_statements.include?('update_author_bios') + client.prepare('update_author_bios', UpdateAuthorBiosSql) + @prepared_statements.add('update_author_bios') + end + result = client.exec_prepared('update_author_bios', query_params) + return result.cmd_tuples + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [get_authors_by_ids_args.param1] + unless @prepared_statements.include?('get_authors_by_ids') + client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) + @prepared_statements.add('get_authors_by_ids') + end + result = client.exec_prepared('get_authors_by_ids', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] + unless @prepared_statements.include?('get_authors_by_ids_and_names') + client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) + @prepared_statements.add('get_authors_by_ids_and_names') + end + result = client.exec_prepared('get_authors_by_ids_and_names', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + copy_authors_args.each do |item| + client.put_copy_data([item.name, item.bio]) + end + end + end + end + + def truncate_authors + @db.with do |client| + unless @prepared_statements.include?('truncate_authors') + client.prepare('truncate_authors', TruncateAuthorsSql) + @prepared_statements.add('truncate_authors') + end + client.exec_prepared('truncate_authors') + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + unless @prepared_statements.include?('create_book') + client.prepare('create_book', CreateBookSql) + @prepared_statements.add('create_book') + end + result = client.exec_prepared('create_book', query_params) + return result.first['id'] + end + end + + def list_all_authors_books + @db.with do |client| + unless @prepared_statements.include?('list_all_authors_books') + client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) + @prepared_statements.add('list_all_authors_books') + end + result = client.exec_prepared('list_all_authors_books').values + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def get_duplicate_authors + @db.with do |client| + unless @prepared_statements.include?('get_duplicate_authors') + client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) + @prepared_statements.add('get_duplicate_authors') + end + result = client.exec_prepared('get_duplicate_authors').values + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + unless @prepared_statements.include?('get_authors_by_book_name') + client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) + @prepared_statements.add('get_authors_by_book_name') + end + result = client.exec_prepared('get_authors_by_book_name', query_params).values + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] + unless @prepared_statements.include?('create_extended_bio') + client.prepare('create_extended_bio', CreateExtendedBioSql) + @prepared_statements.add('create_extended_bio') + end + client.exec_prepared('create_extended_bio', query_params) + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + unless @prepared_statements.include?('get_first_extended_bio_by_type') + client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) + @prepared_statements.add('get_first_extended_bio_by_type') + end + result = client.exec_prepared('get_first_extended_bio_by_type', query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + return entity + end + end + + def truncate_extended_bios + @db.with do |client| + unless @prepared_statements.include?('truncate_extended_bios') + client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) + @prepared_statements.add('truncate_extended_bios') + end + client.exec_prepared('truncate_extended_bios') + end + end + + def insert_extra_types(insert_extra_types_args) + @db.with do |client| + query_params = [ + insert_extra_types_args.c_uuid, + insert_extra_types_args.c_jsonb, + insert_extra_types_args.c_text_array, + insert_extra_types_args.c_integer_array, + insert_extra_types_args.c_timestamptz, + insert_extra_types_args.c_inet, + insert_extra_types_args.c_money, + insert_extra_types_args.c_not_null + ] + unless @prepared_statements.include?('insert_extra_types') + client.prepare('insert_extra_types', InsertExtraTypesSql) + @prepared_statements.add('insert_extra_types') + end + client.exec_prepared('insert_extra_types', query_params) + end + end + + def get_extra_types + @db.with do |client| + unless @prepared_statements.include?('get_extra_types') + client.prepare('get_extra_types', GetExtraTypesSql) + @prepared_statements.add('get_extra_types') + end + result = client.exec_prepared('get_extra_types') + row = result.first + return nil if row.nil? + entity = GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + return entity + end + end + + def truncate_extra_types + @db.with do |client| + unless @prepared_statements.include?('truncate_extra_types') + client.prepare('truncate_extra_types', TruncateExtraTypesSql) + @prepared_statements.add('truncate_extra_types') + end + client.exec_prepared('truncate_extra_types') + end + end end end \ No newline at end of file diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index c0ce377..537df2f 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -5,8 +5,8 @@ module PgCodegen class GetAuthorRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end class GetAuthorArgs @@ -19,8 +19,8 @@ module PgCodegen class ListAuthorsRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end CreateAuthorSql: String @@ -28,14 +28,14 @@ module PgCodegen class CreateAuthorRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end class CreateAuthorArgs attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void end DeleteAuthorSql: String @@ -48,59 +48,300 @@ module PgCodegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer] - attr_reader c_smallint: Integer - attr_reader c_boolean: bool - attr_reader c_integer: Integer - attr_reader c_bigint: Integer - attr_reader c_serial: Integer - attr_reader c_decimal: Float - attr_reader c_numeric: Float - attr_reader c_real: Float - attr_reader c_double_precision: Float - attr_reader c_date: String - attr_reader c_time: String - attr_reader c_timestamp: String - attr_reader c_char: String - attr_reader c_varchar: String - attr_reader c_bytea: Array[Integer] - attr_reader c_text: String - attr_reader c_json: String + attr_reader c_bit: Array[Integer]? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: Float? + attr_reader c_numeric: Float? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_timestamp: String? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: Array[Integer]? + attr_reader c_text: String? + attr_reader c_json: String? def initialize: ( - Array[Integer] c_bit, - Integer c_smallint, - bool c_boolean, - Integer c_integer, - Integer c_bigint, - Integer c_serial, - Float c_decimal, - Float c_numeric, - Float c_real, - Float c_double_precision, - String c_date, - String c_time, - String c_timestamp, - String c_char, - String c_varchar, - Array[Integer] c_bytea, - String c_text, - String c_json + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json ) -> void end + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader offset: Integer + attr_reader limit: Integer + def initialize: ( Integer offset, Integer limit) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader param1: Array[Integer] + def initialize: ( Array[Integer] param1) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader param1: Array[Integer] + attr_reader param2: Array[String] + def initialize: ( Array[Integer] param1, Array[String] param2) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow + attr_reader id: String + def initialize: ( String id) -> void + end + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + class GetFirstExtendedBioByTypeArgs + attr_reader bio_type: String? + def initialize: ( String? bio_type) -> void + end + + TruncateExtendedBiosSql: String + + InsertExtraTypesSql: String + + class InsertExtraTypesArgs + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + GetExtraTypesSql: String + + class GetExtraTypesRow + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + TruncateExtraTypesSql: String + class QuerySql @db: untyped @prepared_statements: Set[String] def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def get_author: (GetAuthorArgs) -> GetAuthorRow? - def list_authors: -> void + def list_authors: -> Array[ListAuthorsRow] def create_author: (CreateAuthorArgs) -> CreateAuthorRow? def delete_author: (DeleteAuthorArgs) -> void def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (InsertExtraTypesArgs) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void end end \ No newline at end of file diff --git a/examples/sqlite3/models.rb b/examples/sqlite3/models.rb new file mode 100644 index 0000000..c96b91e --- /dev/null +++ b/examples/sqlite3/models.rb @@ -0,0 +1,9 @@ +# auto-generated by sqlc - do not edit + +module Sqlite3Codegen + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end \ No newline at end of file diff --git a/examples/sqlite3/models.rbs b/examples/sqlite3/models.rbs new file mode 100644 index 0000000..df9c5ba --- /dev/null +++ b/examples/sqlite3/models.rbs @@ -0,0 +1,17 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end +end \ No newline at end of file diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index c7f128c..55eaad0 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen GetAuthorSql = %q(SELECT id, name, bio FROM authors @@ -33,45 +34,335 @@ class CreateAuthorArgs < Data.define(:name, :bio) class DeleteAuthorArgs < Data.define(:id) end + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + class QuerySql def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def get_author(get_author_args) client = @db query_params = [get_author_args.id] stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + ensure + stmt.close + end end def list_authors client = @db stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close end - return entities end def create_author(create_author_args) client = @db query_params = [create_author_args.name, create_author_args.bio] stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end def delete_author(delete_author_args) client = @db query_params = [delete_author_args.id] stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def list_authors_paged(list_authors_paged_args) + client = @db + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + client = @db + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author_return_id(create_author_return_id_args) + client = @db + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def update_author_bios(update_author_bios_args) + client = @db + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + client = @db + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + client = @db + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def copy_authors(copy_authors_args) + client = @db + copy_authors_args.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) + end + end + + def truncate_authors + client = @db + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + + def create_book(create_book_args) + client = @db + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + client = @db + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') end end end \ No newline at end of file diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index 466fdfd..e6ac24f 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -5,8 +5,8 @@ module Sqlite3Codegen class GetAuthorRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end class GetAuthorArgs @@ -19,16 +19,16 @@ module Sqlite3Codegen class ListAuthorsRow attr_reader id: Integer attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void end CreateAuthorSql: String class CreateAuthorArgs attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void end DeleteAuthorSql: String @@ -38,16 +38,175 @@ module Sqlite3Codegen def initialize: ( Integer id) -> void end + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader offset: Integer + attr_reader limit: Integer + def initialize: ( Integer offset, Integer limit) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader ids: Array[Integer] + def initialize: ( Array[Integer] ids) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader ids: Array[Integer] + attr_reader names: Array[String] + def initialize: ( Array[Integer] ids, Array[String] names) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + class QuerySql @db: untyped def initialize: (untyped) -> void + def transaction: [T] () { (self) -> T } -> T + def get_author: (GetAuthorArgs) -> GetAuthorRow? - def list_authors: -> void + def list_authors: -> Array[ListAuthorsRow] def create_author: (CreateAuthorArgs) -> void def delete_author: (DeleteAuthorArgs) -> void + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + private + def slice_placeholders: (Array[untyped]) -> String end end \ No newline at end of file diff --git a/lib/sqlc_gen_ruby/catalog.rb b/lib/sqlc_gen_ruby/catalog.rb new file mode 100644 index 0000000..de1c6e3 --- /dev/null +++ b/lib/sqlc_gen_ruby/catalog.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative 'code' + +module SqlcGenRuby + # Read-only view of the GenerateRequest catalog: tables (-> model classes, sqlc.embed) and enums. + class Catalog + INTERNAL_SCHEMAS = %w[pg_catalog information_schema pg_temp].freeze + + Table = Struct.new(:schema, :name, :columns, :model_name) + + attr_reader :tables, :enums + + def initialize(proto_catalog) + @default_schema = proto_catalog&.default_schema.to_s + schemas = (proto_catalog&.schemas || []).reject { |s| INTERNAL_SCHEMAS.include?(s.name) } + @tables = schemas.flat_map do |schema| + schema.tables.map do |t| + Table.new(schema.name, t.rel.name, t.columns, nil) + end + end + assign_model_names + # qualified enum name => values; default-schema enums are reachable both bare and qualified + @enums = {} + schemas.each do |schema| + schema.enums.each do |e| + @enums["#{schema.name}.#{e.name}"] = e.vals.to_a + @enums[e.name] = e.vals.to_a if default_schema?(schema.name) + end + end + end + + def enum?(type) = @enums.key?(qualified_name(type)) + + def enum_values(type) = @enums[qualified_name(type)] + + # Enums with their Ruby module names, one entry per distinct enum. + def enum_modules + @enums.reject { |name, _| name.include?('.') && default_schema?(name.split('.').first) } + .map { |name, vals| [Code.camelize(name), vals] } + end + + # embed_table: Identifier(schema, name) as sqlc sends it for sqlc.embed + def table_for(identifier) + @tables.find do |t| + t.name == identifier.name && (identifier.schema.empty? || t.schema == identifier.schema || + (t.schema == @default_schema && identifier.schema == @default_schema)) + end + end + + private + + def default_schema?(name) = name.empty? || name == @default_schema + + # Identifier / column type -> "schema.name" for non default schemas + def qualified_name(type) + name = type.name.to_s.sub(/\Apg_catalog\./, '') + return name if name.include?('.') || type.schema.to_s.empty? || type.schema == 'pg_catalog' + + "#{type.schema}.#{name}" + end + + def assign_model_names + @tables.each do |t| + base = Code.camelize(Code.singularize(t.name)) + t.model_name = default_schema?(t.schema) ? base : Code.camelize(t.schema) + base + end + end + end +end diff --git a/lib/sqlc_gen_ruby/code.rb b/lib/sqlc_gen_ruby/code.rb index 71b61f9..0706a36 100644 --- a/lib/sqlc_gen_ruby/code.rb +++ b/lib/sqlc_gen_ruby/code.rb @@ -35,6 +35,29 @@ def upcase_first(str) str[0].upcase + str[1..] end + # authors => author, policies => policy, statuses => status, addresses => address, books => book + def singularize(word) + case word + when /ies\z/ then word.sub(/ies\z/, 'y') + when /(ss|sh|ch|x|z|u)es\z/ then word.sub(/es\z/, '') + when /(ss|us|is)\z/ then word + when /s\z/ then word.chomp('s') + else word + end + end + + # node_postgres_types => NodePostgresTypes + def camelize(word) + word.split(/[^A-Za-z0-9]+/).reject(&:empty?).map { |w| w[0].upcase + w[1..] }.join + end + + # 'Auto biography' => AUTO_BIOGRAPHY (valid constant name) + def constant_name(value) + name = value.to_s.gsub(/[^A-Za-z0-9]+/, '_').gsub(/\A_+|_+\z/, '').upcase + name = "VALUE_#{name}" if name.empty? || name.match?(/\A[0-9]/) + name + end + def block(header, body) "#{header}\n#{indent(body)}\nend" end diff --git a/lib/sqlc_gen_ruby/drivers/base.rb b/lib/sqlc_gen_ruby/drivers/base.rb index d6ba55a..b4b922b 100644 --- a/lib/sqlc_gen_ruby/drivers/base.rb +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -4,50 +4,75 @@ module SqlcGenRuby module Drivers - # Port of Drivers/DbDriver.cs (+ ColumnMappingConfig.cs, RowDataType.cs, Variable.cs). + # Port of Drivers/DbDriver.cs (+ ColumnMappingConfig.cs, RowDataType.cs, Variable.cs), extended with the hooks + # for :execrows / :execlastid / :copyfrom / sqlc.slice / transactions. # # A driver supplies the DB-specific snippets; Generator/MethodGen assemble the file around them. - # Every `*_code` / `*_type` pair mirrors BuildCode / BuildType of the C# syntax-tree nodes. + # Methods returning "lines" return an Array of Ruby source lines for the body of the generated method (inside + # the connection block). class Base ColumnMapping = Struct.new(:ruby_type, :db_types, :prefix_based) - # Names of the generated variables (Variable.cs): snake_case(AsVar) / '@' + snake_case(AsProperty). - DB = '@db' - CLIENT = 'client' - STMT = 'stmt' - RESULT = 'result' - ROW = 'row' - ENTITY = 'entity' - ENTITIES = 'entities' - QUERY_PARAMS = 'query_params' + # Set by the Generator: gives access to enums (a column of an enum type is a String). + attr_accessor :catalog # Name used for the generated module (`Codegen`) - same as the C# DriverName enum value. def name = raise(NotImplementedError) def required_gems = ['connection_pool', *driver_gems] + # Extra `require`s needed by generated code for these queries (stdlib or gems), beyond required_gems. + def extra_requires(_queries) = [] + + # Extra gems for the generated Gemfile (e.g. `csv`, a bundled gem since Ruby 3.4). + def extra_gems(_queries) = [] + def init_code = raise(NotImplementedError) def init_type = raise(NotImplementedError) + # `def transaction` of the query class: runs the block with all queries on one connection / transaction. + def transaction_code = raise(NotImplementedError) + + def transaction_type = "\ndef transaction: [T] () { (self) -> T } -> T" + # RHS of the generated `Sql` constant (without the constant name). def query_text(query) = query.text def prepare_stmt(_func_name, _const) = raise(NotImplementedError) - def execute_stmt(_func_name, _has_params) = raise(NotImplementedError) + # array_rows: rows are read by position (needed when column names are ambiguous, e.g. sqlc.embed). + def execute_stmt(_func_name, _has_params, array_rows: false) = raise(NotImplementedError) # Whether generated methods borrow the client from a ConnectionPool (`@db.with do |client|`) # or use the single injected connection (`client = @db`). def pooling_enabled? = true + # Whether the prepared `stmt` must be closed after use (mysql2 / sqlite3 prepare on every call; unclosed + # statements leak server resources and make SQLite refuse to COMMIT). + def closes_statements? = false + # How rows are read: :hash => row['col'], :array => row[0] def row_data_type = :hash - def supports_exec_last_id? = false + # sqlc.slice is only offered by sqlc for MySQL / SQLite (`IN (/*SLICE:ids*/?)`). + def supports_slice? = false + + def exec_rows_lines(_exec) = raise(NotImplementedError) + + def exec_last_id_lines(_exec, _query) = raise(NotImplementedError) - def column_type(column) - db_type = column.type.name.downcase # from sqlc - char(5), int4, float8, blob, ... + def copy_from_lines(_query, _table, _columns) = raise(NotImplementedError) + + # Source of helper methods the class needs for these queries: [[code, rbs], ...] + def helper_methods(_queries) = [] + + # Ruby/RBS type of a column's DB type (no nullability / array wrapper). Enums are Strings. + # With fallback: nil unknown types return it instead of raising (used for models, which cover every table). + def column_type(column, fallback: :raise) + return 'String' if catalog&.enum?(column.type) + + db_type = normalize_type(column.type.name) column_mappings.each do |mapping| matched = if mapping.prefix_based mapping.db_types.any? { |d| d.start_with?(db_type) } @@ -56,6 +81,8 @@ def column_type(column) end return mapping.ruby_type if matched end + return fallback unless fallback == :raise + raise ArgumentError, "Unsupported column type: #{column.type.name}" end @@ -66,6 +93,9 @@ def query_text_constant(query) private + # from sqlc - char(5), int4, pg_catalog.int8, BLOB, ... + def normalize_type(type_name) = type_name.downcase.delete_prefix('pg_catalog.') + def driver_gems = raise(NotImplementedError) def column_mappings = raise(NotImplementedError) @@ -73,6 +103,11 @@ def column_mappings = raise(NotImplementedError) def mapping(ruby_type, db_types, prefix_based: false) ColumnMapping.new(ruby_type, db_types, prefix_based) end + + # 'authors' / 'extended.bios' + def table_name(identifier) + identifier.schema.to_s.empty? ? identifier.name : "#{identifier.schema}.#{identifier.name}" + end end end end diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb index aac5f91..d95210b 100644 --- a/lib/sqlc_gen_ruby/drivers/mysql2.rb +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -19,14 +19,92 @@ def init_type 'def initialize: (Hash[String, String], Hash[String, String]) -> void' end + # mysql2 has no transaction helper: BEGIN / COMMIT / ROLLBACK on the (reentrantly pooled) connection. + def transaction_code + guarded = [ + 'begin', + Code.indent("result = yield self\nclient.query('COMMIT')\nresult"), + 'rescue Exception', + Code.indent("client.query('ROLLBACK')\nraise"), + 'end' + ].join("\n") + Code.block('def transaction', Code.block('@db.with do |client|', "client.query('BEGIN')\n#{guarded}")) + end + def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" - def execute_stmt(_func_name, has_params) = has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' + def execute_stmt(_func_name, has_params, array_rows: false) + args = [] + args << '*query_params' if has_params + args << 'as: :array' if array_rows + args.empty? ? 'stmt.execute' : "stmt.execute(#{args.join(', ')})" + end + + def closes_statements? = true + + def supports_slice? = true - def supports_exec_last_id? = true + def exec_rows_lines(exec) = [exec, 'return stmt.affected_rows'] + + def exec_last_id_lines(exec, _query) = [exec, 'return client.last_id'] + + # LOAD DATA LOCAL INFILE from a temporary CSV (the client needs `local_infile: true`, the server + # `local_infile=1`). + def copy_from_lines(query, table, columns) + args = Code.snake_case("#{query.name}Args") + write = Code.block("#{args}.each do |item|", + "file.write(csv_line([#{columns.map { |c| "item.#{c}" }.join(', ')}]))") + load_data = format(LOAD_DATA, table: table, columns: columns.join(', ')) + [ + "return if #{args}.empty?", + Code.block("Tempfile.create(['#{Code.snake_case(query.name)}', '.csv']) do |file|", + "#{write}\nfile.flush\n#{load_data}") + ] + end + + def extra_requires(queries) = copy_from?(queries) ? %w[csv tempfile] : [] + + def extra_gems(queries) = copy_from?(queries) ? %w[csv] : [] + + def helper_methods(queries) + return [] unless copy_from?(queries) + + [[CSV_LINE_CODE, 'def csv_line: (Array[untyped]) -> String']] + end + + # Template of the generated statement (single-quoted heredoc: backslashes are copied verbatim). + LOAD_DATA = <<~'RUBY'.chomp + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE %
s FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (%s)") + RUBY + + # \N is LOAD DATA's NULL; backslashes are doubled because the statement uses ESCAPED BY '\'. + CSV_LINE_CODE = [ + 'def csv_line(values)', + Code.indent( + [ + 'fields = values.map do |value|', + Code.indent( + [ + 'case value', + "when nil then '\\N'", + "when true then '1'", + "when false then '0'", + "when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N')", + "else value.to_s.gsub('\\\\') { '\\\\\\\\' }", + 'end' + ].join("\n") + ), + 'end', + 'CSV.generate_line(fields)' + ].join("\n") + ), + 'end' + ].join("\n") private + def copy_from?(queries) = queries.any? { |q| q.cmd == ':copyfrom' } + def driver_gems = %w[mysql2] def column_mappings @@ -34,7 +112,7 @@ def column_mappings mapping('Array[Integer]', %w[binary bit blob longblob mediumblob tinyblob varbinary]), mapping('String', %w[char date datetime decimal longtext mediumtext text time timestamp tinytext varchar json]), - mapping('Integer', %w[bigint int mediumint smallint tinyint year]), + mapping('Integer', %w[bigint int mediumint smallint tinyint year integer]), mapping('Float', %w[double float]) ] end diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index 1c84acf..f272ab8 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -4,14 +4,18 @@ module SqlcGenRuby module Drivers - # Port of Drivers/PgDriver.cs. + # Port of Drivers/PgDriver.cs. Each *_code / *_type pair mirrors the + # BuildCode / BuildType of the C# syntax tree nodes. class Pg < Base def name = 'Pg' def init_code pool_body = [ 'client = PG.connect(**pg_params)', - 'client.type_map_for_results = PG::BasicTypeMapForResults.new client', + 'type_map = PG::BasicTypeMapForResults.new client', + 'type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings', + 'client.type_map_for_results = type_map', + 'client.type_map_for_queries = PG::BasicTypeMapForQueries.new client', 'client' ].join("\n") pool = "ConnectionPool.new(**connection_pool_params) {\n#{Code.indent(pool_body)}\n}" @@ -24,10 +28,9 @@ def init_type 'def initialize: (Hash[String, String], Hash[String, String]) -> void' end - # sqlc emits `?` placeholders; PostgreSQL wants $1, $2, ... - def query_text(query) - counter = 0 - query.text.gsub('?') { "$#{counter += 1}" } + # PG::Connection#transaction commits when the block returns and rolls back when it raises. + def transaction_code + Code.block('def transaction', Code.block('@db.with do |client|', Code.block('client.transaction do', 'yield self'))) end def prepare_stmt(func_name, const) @@ -35,8 +38,31 @@ def prepare_stmt(func_name, const) Code.block("unless @prepared_statements.include?('#{func_name}')", body) end - def execute_stmt(func_name, has_params) - "client.exec_prepared('#{func_name}'#{has_params ? ', query_params' : ''})" + def execute_stmt(func_name, has_params, array_rows: false) + call = "client.exec_prepared('#{func_name}'#{has_params ? ', query_params' : ''})" + array_rows ? "#{call}.values" : call + end + + def exec_rows_lines(exec) = ["result = #{exec}", 'return result.cmd_tuples'] + + # Relies on a user-written `RETURNING `. + def exec_last_id_lines(exec, query) + if query.columns.empty? + raise ArgumentError, "#{query.name}: :execlastid on PostgreSQL needs a RETURNING clause" + end + + ["result = #{exec}", "return result.first['#{query.columns.first.name}']"] + end + + # COPY ... FROM STDIN, one row at a time through the pg gem's CopyRow encoder. + def copy_from_lines(query, table, columns) + args = Code.snake_case("#{query.name}Args") + put = Code.block("#{args}.each do |item|", + "client.put_copy_data([#{columns.map { |c| "item.#{c}" }.join(', ')}])") + [ + 'encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries)', + Code.block("client.copy_data('COPY #{table} (#{columns.join(', ')}) FROM STDIN', encoder) do", put) + ] end private @@ -48,9 +74,12 @@ def column_mappings mapping('bool', %w[bool boolean]), mapping('Array[Integer]', %w[binary bit bytea blob longblob mediumblob tinyblob varbinary]), mapping('String', %w[char date datetime longtext mediumtext text bpchar time timestamp tinytext - varchar json]), - mapping('Integer', %w[int2 int4 int8 serial bigserial]), - mapping('Float', %w[numeric float4 float8 decimal]) + varchar json uuid money macaddr macaddr8 interval xml name citext]), + mapping('Integer', %w[int2 int4 int8 serial bigserial integer smallint bigint int smallserial]), + mapping('Float', %w[numeric float4 float8 decimal real double]), + mapping('Time', %w[timestamptz]), + mapping('IPAddr', %w[inet cidr]), + mapping('untyped', %w[jsonb]) ] end end diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb index 2e0604d..ea267fb 100644 --- a/lib/sqlc_gen_ruby/drivers/sqlite3.rb +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -7,20 +7,54 @@ module Drivers # Port of Drivers/Sqlite3Driver.cs. A single SQLite3::Database is injected, so there is no connection # pool, and rows are read by index (the sqlite3 gem returns arrays by default). class Sqlite3 < Base + # SQLITE_MAX_VARIABLE_NUMBER was 999 before SQLite 3.32; stay below it for :copyfrom batches. + MAX_BOUND_PARAMETERS = 999 + def name = 'Sqlite3' def init_code = Code.block('def initialize(db)', '@db = db') def init_type = "@db: untyped\ndef initialize: (untyped) -> void" + def transaction_code = Code.block('def transaction', Code.block('@db.transaction do', 'yield self')) + def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" - def execute_stmt(_func_name, has_params) = has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' + def execute_stmt(_func_name, has_params, array_rows: false) + has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' + end def pooling_enabled? = false def row_data_type = :array + def closes_statements? = true + + def supports_slice? = true + + def exec_rows_lines(exec) = [exec, 'return client.changes'] + + # Relies on a user-written `RETURNING `. + def exec_last_id_lines(exec, query) + if query.columns.empty? + raise ArgumentError, "#{query.name}: :execlastid on SQLite needs a RETURNING clause" + end + + ["result = #{exec}", 'row = result.first', 'return row[0]'] + end + + # One multi-row `INSERT ... VALUES (?, ?), (?, ?), ...` per batch. + def copy_from_lines(query, table, columns) + args = Code.snake_case("#{query.name}Args") + rows_per_batch = [MAX_BOUND_PARAMETERS / columns.size, 1].max + placeholders = "(#{(['?'] * columns.size).join(', ')})" + body = [ + %(sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES \#{Array.new(batch.size, '#{placeholders}').join(', ')}"), + "client.execute(sql, batch.flat_map { |item| [#{columns.map { |c| "item.#{c}" }.join(', ')}] })" + ].join("\n") + [Code.block("#{args}.each_slice(#{rows_per_batch}) do |batch|", body)] + end + private def driver_gems = %w[sqlite3] diff --git a/lib/sqlc_gen_ruby/generator.rb b/lib/sqlc_gen_ruby/generator.rb index 3e30954..7913d69 100644 --- a/lib/sqlc_gen_ruby/generator.rb +++ b/lib/sqlc_gen_ruby/generator.rb @@ -3,30 +3,50 @@ require_relative 'code' require_relative 'options' require_relative 'plugin_pb' +require_relative 'catalog' +require_relative 'type_resolver' +require_relative 'row_layout' require_relative 'method_gen' require_relative 'drivers/pg' require_relative 'drivers/mysql2' require_relative 'drivers/sqlite3' module SqlcGenRuby - # Ruby port of CodeGenerator/CodeGenerator.cs + Drivers/MethodGen.cs. + # Ruby port of CodeGenerator/CodeGenerator.cs, extended with models, enums, sqlc.embed, ... class Generator - include Code - AUTO_GENERATED = '# auto-generated by sqlc - do not edit' + MODELS_FILE = 'models' def generate(request) @options = Options.new(request.plugin_options) @driver = instantiate_driver + @catalog = Catalog.new(request.catalog) + @driver.catalog = @catalog + @types = TypeResolver.new(@driver, @options) + @layout = RowLayout.new(@driver, @catalog, @types) + @method_gen = MethodGen.new(@driver, @layout, @types) + + name_anonymous_params(request.queries) + models = models_files files = request.queries.group_by { |q| class_name_for(q.filename) }.flat_map do |class_name, queries| - generate_files(queries, class_name) + generate_files(queries, class_name, with_models: !models.empty?) end - files << gemfile if @options.generate_gemfile + files.concat(models) + files << gemfile(request.queries) if @options.generate_gemfile Plugin::GenerateResponse.new(files: files) end private + # `ANY($1::BIGINT[])` and other expressions arrive without a name; use sqlc.arg('ids') to pick one. + def name_anonymous_params(queries) + queries.each do |query| + query.params.each do |param| + param.column.name = "param#{param.number}" if param.column.name.empty? + end + end + end + def instantiate_driver case @options.driver_name when 'mysql2' then Drivers::Mysql2.new @@ -36,6 +56,8 @@ def instantiate_driver end end + def module_name = "#{@driver.name}Codegen" + # query.sql => QuerySql def class_name_for(filename) ext = File.extname(filename) @@ -44,15 +66,14 @@ def class_name_for(filename) Code.upcase_first(File.basename(filename, ext)) + Code.upcase_first(ext[1..]) end - def generate_files(queries, class_name) - module_name = "#{@driver.name}Codegen" + def generate_files(queries, class_name, with_models:) members = queries.flat_map { |q| query_members(q) } klass = class_members(queries) base = Code.snake_case(class_name) code_body = (members.map(&:first) + [Code.block("class #{class_name}", klass.map(&:first).join("\n\n"))]) - gems = @driver.required_gems.map { |g| "require '#{g}'" }.join("\n") - code = "#{AUTO_GENERATED}\n#{gems}\n\n#{Code.block("module #{module_name}", code_body.join("\n\n"))}" + requires = requires_for(queries, with_models) + code = "#{AUTO_GENERATED}\n#{requires}\n\n#{Code.block("module #{module_name}", code_body.join("\n\n"))}" files = [Plugin::File.new(name: "#{base}.rb", contents: code)] if @options.generate_types @@ -63,23 +84,49 @@ def generate_files(queries, class_name) files end + def requires_for(queries, with_models) + lines = (@driver.required_gems + @driver.extra_requires(queries)).map { |g| "require '#{g}'" } + lines << "require_relative '#{MODELS_FILE}'" if with_models + lines.join("\n") + end + # [[code, rbs_or_nil], ...] for the SQL constant, row class, and args class. def query_members(query) - members = [[@driver.query_text_constant(query), "#{query.name}Sql: String"]] - members << dataclass("#{Code.upcase_first(query.name)}Row", query.columns) unless query.columns.empty? + members = [] + unless query.cmd == ':copyfrom' # copied rows are written by generated code, there is no SQL constant to run + members << [@driver.query_text_constant(query), "#{query.name}Sql: String"] + end + members << dataclass("#{Code.upcase_first(query.name)}Row", row_pairs(query)) unless query.columns.empty? unless query.params.empty? - members << dataclass("#{Code.upcase_first(query.name)}Args", query.params.map(&:column)) + members << dataclass("#{Code.upcase_first(query.name)}Args", param_pairs(query)) end members end + def row_pairs(query) = @layout.entries(query).map { |e| [e.attr, e.type] } + + def param_pairs(query) + query.params.map do |p| + type = p.column.is_sqlc_slice ? @types.slice_type(p.column, query.name) : @types.type(p.column, query_name: query.name) + [p.column.name, type] + end + end + def class_members(queries) - method_gen = MethodGen.new(@driver) - [[@driver.init_code, @driver.init_type]] + queries.map { |q| [method_gen.code(q), method_gen.type(q)] } + members = [[@driver.init_code, @driver.init_type], + [@driver.transaction_code, @driver.transaction_type]] + members.concat(queries.map { |q| [@method_gen.code(q), @method_gen.type(q)] }) + helpers = @method_gen.helper_methods(queries) + unless helpers.empty? + members << ["private\n\n#{helpers.map(&:first).join("\n\n")}", + "\nprivate\n#{helpers.map(&:last).join("\n")}"] + end + members end - def dataclass(name, columns) - pairs = columns.map { |c| [c.name, @driver.column_type(c)] } + # --- data classes (Data.define / Struct.new) --- + + def dataclass(name, pairs) dupes = pairs.map(&:first).tally.select { |_, n| n > 1 }.keys raise ArgumentError, "duplicate column names in #{name}: #{dupes.join(', ')}" unless dupes.empty? @@ -104,8 +151,48 @@ def new_struct(name, pairs) [code, "class #{name} < ::Struct\n#{accessors}\n#{init}\nend"] end - def gemfile - gems = @driver.required_gems.map { |g| "gem '#{g}'" }.join("\n") + # --- models.rb: one class per table + one module per enum --- + + def models_files + members = @catalog.enum_modules.map { |name, vals| enum_module(name, vals) } + @catalog.tables.each do |table| + next if table.columns.empty? + + pairs = table.columns.map { |c| [c.name, @types.type(c, fallback: 'untyped')] } + members << dataclass(table.model_name, pairs) + end + return [] if members.empty? + + code = "#{AUTO_GENERATED}\n\n#{Code.block("module #{module_name}", members.map(&:first).join("\n\n"))}" + files = [Plugin::File.new(name: "#{MODELS_FILE}.rb", contents: code)] + if @options.generate_types + types = "#{AUTO_GENERATED}\n#{Code.block("module #{module_name}", members.map(&:last).join("\n\n"))}" + files << Plugin::File.new(name: "#{MODELS_FILE}.rbs", contents: types) + end + files + end + + # module BioType; AUTOBIOGRAPHY = 'Autobiography'; ...; ALL = [...].freeze; end + def enum_module(name, values) + consts = unique_constants(values) + code = consts.map { |const, value| "#{const} = #{value.inspect}" } + code << "ALL = [#{consts.map(&:first).join(', ')}].freeze" + rbs = consts.map { |const, _| "#{const}: String" } + rbs << 'ALL: Array[String]' + [Code.block("module #{name}", code.join("\n")), Code.block("module #{name}", rbs.join("\n"))] + end + + def unique_constants(values) + seen = Hash.new(0) + values.map do |value| + const = Code.constant_name(value) + seen[const] += 1 + [seen[const] == 1 ? const : "#{const}_#{seen[const]}", value] + end + end + + def gemfile(queries) + gems = (@driver.required_gems + @driver.extra_gems(queries)).uniq.map { |g| "gem '#{g}'" }.join("\n") Plugin::File.new(name: 'Gemfile', contents: "source 'https://rubygems.org'\n\n#{gems}") end end diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb index 51d5031..084765c 100644 --- a/lib/sqlc_gen_ruby/method_gen.rb +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -6,47 +6,113 @@ module SqlcGenRuby # Port of Drivers/MethodGen.cs: builds the code (and RBS signature) of one generated query method, # parameterised by the driver's pooling / row-access / statement flavour. class MethodGen - def initialize(driver) + SLICE_HELPER = [ + "def slice_placeholders(values)\n\tvalues.empty? ? 'NULL' : Array.new(values.size, '?').join(', ')\nend", + 'def slice_placeholders: (Array[untyped]) -> String' + ].freeze + + # layout: Generator#row_layout; types: TypeResolver + def initialize(driver, layout, types) @driver = driver + @layout = layout + @types = types end def code(query) func = Code.snake_case(query.name) args_var = Code.snake_case("#{query.name}Args") - has_params = !query.params.empty? - - statements = [] - statements << query_params(args_var, query.params) if has_params - statements << @driver.prepare_stmt(func, "#{query.name}Sql") - statements.concat(command_statements(query, func, has_params)) - - params = has_params ? "(#{args_var})" : '' + statements = query.cmd == ':copyfrom' ? copy_statements(query) : query_statements(query, func, args_var) + params = query.params.empty? ? '' : "(#{args_var})" Code.block("def #{func}#{params}", method_body(statements, query.cmd)) end def type(query) func = Code.snake_case(query.name) - params = query.params.empty? ? '' : "(#{query.name}Args) " + params = if query.cmd == ':copyfrom' then "(Array[#{query.name}Args]) " + elsif query.params.empty? then '' + else "(#{query.name}Args) " + end "\ndef #{func}: #{params}-> #{return_type(query)}" end + # [[code, rbs]] of private helper methods the generated class needs for these queries. + def helper_methods(queries) + helpers = @driver.helper_methods(queries) + helpers << SLICE_HELPER if queries.any? { |q| slice?(q) } + helpers + end + private + def slice?(query) = query.params.any? { |p| p.column.is_sqlc_slice } + def return_type(query) case query.cmd when ':one' then "#{query.name}Row?" - when ':execlastid' then 'Integer' + when ':many' then "Array[#{query.name}Row]" + when ':execrows' then 'Integer' + when ':execlastid' then last_id_type(query) else 'void' end end + # MySQL: client.last_id (Integer); PostgreSQL / SQLite: the RETURNING column + def last_id_type(query) + return 'Integer' if query.columns.empty? + + @types.type(query.columns.first, query_name: query.name) + end + + def copy_statements(query) + raise ArgumentError, "#{query.name}: :copyfrom without a target table" if query.insert_into_table.nil? + + table = query.insert_into_table + name = table.schema.to_s.empty? ? table.name : "#{table.schema}.#{table.name}" + @driver.copy_from_lines(query, name, query.params.map { |p| p.column.name }) + end + + def query_statements(query, func, args_var) + has_params = !query.params.empty? + if slice?(query) && !@driver.supports_slice? + raise ArgumentError, "#{query.name}: sqlc.slice is not supported by the #{@driver.name} driver" + end + + statements = [] + statements << query_params(args_var, query.params) if has_params + sql_const = "#{query.name}Sql" + if slice?(query) + statements << slice_sql(sql_const, args_var, query.params) + sql_const = 'sql' + end + statements << @driver.prepare_stmt(func, sql_const) + commands = command_statements(query, func, has_params) + if @driver.closes_statements? + statements << Code.block('begin', commands.join("\n")).sub(/\nend\z/, "\nensure\n\tstmt.close\nend") + else + statements.concat(commands) + end + statements + end + def query_params(args_var, params) - list = Code.comma_join(params.map { |p| "#{args_var}.#{p.column.name}" }) - "query_params = [#{list}]" + list = params.map do |p| + access = "#{args_var}.#{p.column.name}" + p.column.is_sqlc_slice ? "*#{access}" : access + end + "query_params = [#{Code.comma_join(list)}]" + end + + # sql = FooSql.sub('/*SLICE:ids*/?', slice_placeholders(args.ids)).sub(...) + def slice_sql(sql_const, args_var, params) + subs = params.select { |p| p.column.is_sqlc_slice }.map do |p| + ".sub('/*SLICE:#{p.column.name}*/?', slice_placeholders(#{args_var}.#{p.column.name}))" + end + "sql = #{sql_const}#{subs.join}" end def command_statements(query, func, has_params) - exec = @driver.execute_stmt(func, has_params) + array_rows = @layout.array_rows?(query) + exec = @driver.execute_stmt(func, has_params, array_rows: array_rows) case query.cmd when ':one' ["result = #{exec}", @@ -59,34 +125,37 @@ def command_statements(query, func, has_params) 'entities = []', Code.block('result.each do |row|', "entities << #{new_row(query)}"), 'return entities'] - when ':exec' - [exec] - when ':execlastid' - unless @driver.supports_exec_last_id? - raise ArgumentError, "the #{query.cmd} command is not supported by the #{@driver.name} driver" - end - - [exec, 'return client.last_id'] - else - raise ArgumentError, "unsupported query command: #{query.cmd}" + when ':exec' then [exec] + when ':execrows' then @driver.exec_rows_lines(exec) + when ':execlastid' then @driver.exec_last_id_lines(exec, query) + else raise ArgumentError, "unsupported query command: #{query.cmd}" end end - # :execlastid is always pooled (the C# ExecLastIdDeclare had no non-pooled variant). - def method_body(statements, cmd) + # :execlastid / :copyfrom use the pool even where :one/:many/:exec would not (they were pooled in the C# + # implementation too). + def method_body(statements, _cmd) body = statements.join("\n") - return Code.block('@db.with do |client|', body) if @driver.pooling_enabled? || cmd == ':execlastid' + return Code.block('@db.with do |client|', body) if @driver.pooling_enabled? "client = @db\n#{body}" end def new_row(query) - args = Code.comma_join(query.columns.each_with_index.map { |c, i| row_access(c, i) }) - "#{query.name}Row.new(#{args})" - end - - def row_access(column, index) - @driver.row_data_type == :hash ? "row['#{column.name}']" : "row[#{index}]" + array = @layout.array_rows?(query) + index = 0 + args = @layout.entries(query).map do |entry| + if entry.table + values = entry.table.columns.each_index.map { |i| "row[#{index + i}]" } + index += values.size + "#{entry.table.model_name}.new(#{Code.comma_join(values)})" + else + access = array ? "row[#{index}]" : "row['#{entry.column.name}']" + index += 1 + access + end + end + "#{query.name}Row.new(#{Code.comma_join(args)})" end end end diff --git a/lib/sqlc_gen_ruby/options.rb b/lib/sqlc_gen_ruby/options.rb index b02c0a1..4c88afb 100644 --- a/lib/sqlc_gen_ruby/options.rb +++ b/lib/sqlc_gen_ruby/options.rb @@ -8,7 +8,10 @@ module SqlcGenRuby class Options DRIVERS = %w[mysql2 pg sqlite3].freeze - attr_reader :driver_name, :ruby_version, :generate_gemfile, :generate_types + # `overrides:` entry - column is ":" or "*:"; type/not_null come from `ruby_type:` + Override = Struct.new(:query, :column, :type, :not_null) + + attr_reader :driver_name, :ruby_version, :generate_gemfile, :generate_types, :overrides def initialize(plugin_options_bytes) text = plugin_options_bytes.to_s.dup.force_encoding(Encoding::UTF_8) @@ -21,10 +24,38 @@ def initialize(plugin_options_bytes) @generate_gemfile = fetch_bool(raw, 'generateGemfile', false) # not generating Gemfile by default @generate_types = fetch_bool(raw, 'generateTypes', true) @ruby_version = RubyVersion.parse(fetch_string(raw, 'rubyVersion', default: '3.3')) + @overrides = parse_overrides(raw.fetch('overrides', [])) + end + + # The override matching a result column / parameter of a query: "Query:col" wins over "*:col". + def override_for(query_name, column_name) + overrides.find { |o| o.query == query_name && o.column == column_name } || + overrides.find { |o| o.query == '*' && o.column == column_name } end private + def parse_overrides(list) + raise ArgumentError, 'option overrides must be a list' unless list.is_a?(Array) + + list.map do |entry| + raise ArgumentError, 'each override must be an object' unless entry.is_a?(Hash) + + query, column = entry['column'].to_s.split(':', 2) + raise ArgumentError, "override column must look like ':' or '*:'" if column.to_s.empty? + + type = entry['ruby_type'] + raise ArgumentError, "override #{entry['column']} needs ruby_type.type" unless type.is_a?(Hash) && type['type'].is_a?(String) + + not_null = type['notNull'] + unless [nil, true, false].include?(not_null) + raise ArgumentError, "override #{entry['column']}: notNull must be a boolean" + end + + Override.new(query, column, type['type'], not_null) + end + end + def fetch_string(raw, key, default: nil, required: false) raise ArgumentError, "missing required option: #{key}" if required && !raw.key?(key) diff --git a/lib/sqlc_gen_ruby/plugin_pb.rb b/lib/sqlc_gen_ruby/plugin_pb.rb index 52a223c..1745d1a 100644 --- a/lib/sqlc_gen_ruby/plugin_pb.rb +++ b/lib/sqlc_gen_ruby/plugin_pb.rb @@ -54,8 +54,36 @@ class Query < P field :insert_into_table, 8, :message, klass: Identifier end + class Enum < P + field :name, 1, :string + field :vals, 2, :string, repeated: true + field :comment, 3, :string + end + + class Table < P + field :rel, 1, :message, klass: Identifier + field :columns, 2, :message, repeated: true, klass: Column + field :comment, 3, :string + end + + class Schema < P + # composite_types(5) is skipped + field :comment, 1, :string + field :name, 2, :string + field :tables, 3, :message, repeated: true, klass: Table + field :enums, 4, :message, repeated: true, klass: Enum + end + + class Catalog < P + field :comment, 1, :string + field :default_schema, 2, :string + field :name, 3, :string + field :schemas, 4, :message, repeated: true, klass: Schema + end + class GenerateRequest < P - # settings(1) and catalog(2) are skipped: the generator does not need them. + # settings(1) is skipped: the generator does not need it. + field :catalog, 2, :message, klass: Catalog field :queries, 3, :message, repeated: true, klass: Query field :sqlc_version, 4, :string field :plugin_options, 5, :bytes diff --git a/lib/sqlc_gen_ruby/row_layout.rb b/lib/sqlc_gen_ruby/row_layout.rb new file mode 100644 index 0000000..0dcb8cf --- /dev/null +++ b/lib/sqlc_gen_ruby/row_layout.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative 'code' + +module SqlcGenRuby + # How the columns of a query become attributes of its `Row` class. + # Plain columns map 1:1; a `sqlc.embed(table)` column expands (in the SQL) to all columns of that table and becomes + # one attribute holding the table's model, so such rows are read by position. + class RowLayout + Entry = Struct.new(:attr, :type, :column, :table) + + def initialize(driver, catalog, types) + @driver = driver + @catalog = catalog + @types = types + @cache = {} + end + + def entries(query) + @cache[query.object_id] ||= build(query) + end + + # Names of result columns can be ambiguous (embed, joins), positions never are. + def array_rows?(query) + @driver.row_data_type == :array || query.columns.any? { |c| embed?(c) } + end + + private + + def embed?(column) = !column.embed_table.nil? && !column.embed_table.name.empty? + + def build(query) + taken = query.columns.reject { |c| embed?(c) }.map(&:name) + query.columns.map do |column| + if embed?(column) + table = @catalog.table_for(column.embed_table) or + raise ArgumentError, "#{query.name}: sqlc.embed of unknown table #{column.embed_table.name}" + Entry.new(unique_name(taken, Code.singularize(column.name)), table.model_name, column, table) + else + Entry.new(column.name, @types.type(column, query_name: query.name), column, nil) + end + end.tap { |list| reject_duplicate_columns(query, list) } + end + + # first `author`, then `author2`, `author3`, ... (only embedded tables are renamed) + def unique_name(taken, base) + name = base + suffix = 1 + name = "#{base}#{suffix += 1}" while taken.include?(name) + taken << name + name + end + + def reject_duplicate_columns(query, list) + names = list.map(&:attr) + dupes = names.tally.select { |_, n| n > 1 }.keys + raise ArgumentError, "duplicate column names in #{query.name}Row: #{dupes.join(', ')}" unless dupes.empty? + end + end +end diff --git a/lib/sqlc_gen_ruby/type_resolver.rb b/lib/sqlc_gen_ruby/type_resolver.rb new file mode 100644 index 0000000..32cdae2 --- /dev/null +++ b/lib/sqlc_gen_ruby/type_resolver.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module SqlcGenRuby + # Full RBS type of a result column / parameter: DB type mapping (driver) + array + nullability + `overrides:`. + class TypeResolver + def initialize(driver, options) + @driver = driver + @options = options + end + + # query_name: used to look up `overrides:` entries; nil for model (table) columns. + # fallback: see Drivers::Base#column_type. + def type(column, query_name: nil, fallback: :raise) + override = query_name && @options.override_for(query_name, column.name) + base = override ? override.type : @driver.column_type(column, fallback: fallback) + base = "Array[#{base}]" if column.is_array && !override + not_null?(column, override) || base == 'untyped' ? base : "#{base}?" + end + + # A sqlc.slice parameter is passed as an Array of the element type. + def slice_type(column, query_name) + override = @options.override_for(query_name, column.name) + "Array[#{override ? override.type : @driver.column_type(column)}]" + end + + private + + def not_null?(column, override) + override&.not_null.nil? ? column.not_null : override.not_null + end + end +end diff --git a/scripts/capture_request.rb b/scripts/capture_request.rb index 1df6442..a82bbce 100755 --- a/scripts/capture_request.rb +++ b/scripts/capture_request.rb @@ -2,7 +2,8 @@ # frozen_string_literal: true # A sqlc *process plugin* that records the GenerateRequest sqlc sends it, to refresh test/fixtures/*.bin. -# The (large, unused) catalog field is stripped; every other byte is exactly what sqlc sent. +# What is kept is what sqlc sent (queries, plugin options, catalog) minus the settings and the built-in +# schemas (pg_catalog & co.), which are large and unused. The file is re-encoded by the plugin's own codec. # # cat > /tmp/capture-pg.sh <<'SH' # #!/bin/sh @@ -10,19 +11,11 @@ # SH # chmod +x /tmp/capture-pg.sh # then point a sqlc process plugin `cmd:` at it and run `sqlc generate` $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) -require 'sqlc_gen_ruby/proto' +require 'sqlc_gen_ruby/catalog' +require 'sqlc_gen_ruby/plugin_pb' -CATALOG_FIELD = 2 - -data = $stdin.binmode.read -reader = SqlcGenRuby::Proto::Reader.new(data) -out = ''.b -until reader.eof? - start = reader.pos - number, wire = reader.read_tag - reader.skip(wire) - out << data.byteslice(start, reader.pos - start) unless number == CATALOG_FIELD -end -File.binwrite(ARGV.fetch(0), out) +request = SqlcGenRuby::Plugin::GenerateRequest.decode($stdin.binmode.read) +request.catalog.schemas.reject! { |s| SqlcGenRuby::Catalog::INTERNAL_SCHEMAS.include?(s.name) } +File.binwrite(ARGV.fetch(0), request.encode) # sqlc expects a (possibly empty) GenerateResponse back $stdout.binmode.write('') diff --git a/test/codec_test.rb b/test/codec_test.rb index 09848af..49faa54 100644 --- a/test/codec_test.rb +++ b/test/codec_test.rb @@ -13,22 +13,22 @@ class CodecTest < Minitest::Test include PluginHelper - def test_official_request_decodes_and_generates_same_output_as_captured_one - official = Fixture.k('GenerateRequest').encode(Fixture.request) - out, err, status = run_plugin(official) + # request encoded by the official gem -> plugin (own decoder) -> response decoded by both implementations + def test_official_request_and_response_agree_with_own_codec + out, err, status = run_plugin(Fixture.k('GenerateRequest').encode(Fixture.request)) assert status.success?, err - files = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } - %w[query_sql.rb query_sql.rbs].each do |name| - assert_equal normalize(File.read(File.join(ROOT, 'examples/pg', name))), normalize(files.fetch(name)) - end + official = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } + own = Plugin::GenerateResponse.decode(out).files.to_h { |f| [f.name, f.contents] } + assert_equal official, own + assert_equal %w[query_sql.rb query_sql.rbs], own.keys.sort + assert_includes own.fetch('query_sql.rb'), 'GetAuthorSql = %q(SELECT id, name, bio FROM authors' end - def test_captured_sqlc_request_roundtrips_through_the_codec - bytes = File.binread(File.join(FIXTURES, 'pg.bin')) - decoded = Plugin::GenerateRequest.decode(bytes) - official = Fixture.k('GenerateRequest').decode(Fixture.k('GenerateRequest').encode(Fixture.request)) - assert_equal official.queries.map(&:name), %w[GetAuthor ListAuthors CreateAuthor DeleteAuthor Test] - refute_empty decoded.queries - assert_equal decoded.queries.size, Plugin::GenerateRequest.decode(decoded.encode).queries.size + def test_own_encoding_is_readable_by_the_official_implementation + request = load_request('pg') + # the official fixture schema only models a subset of the request; unknown fields must be skipped + decoded = Fixture.k('GenerateRequest').decode(request.encode) + assert_equal request.queries.map(&:name), decoded.queries.map(&:name) + assert_equal request.queries.first.params.first.column.name, decoded.queries.first.params.first.column.name end end diff --git a/test/extended_golden_test.rb b/test/extended_golden_test.rb index 38bb8c8..c8ab31e 100644 --- a/test/extended_golden_test.rb +++ b/test/extended_golden_test.rb @@ -1,24 +1,32 @@ # frozen_string_literal: true require_relative 'test_helper' +require_relative 'synthetic_requests' -# Snapshots of the C# plugin's output (recorded while it still existed) for the sqlc-captured queries plus -# synthetic ones (5+ params/columns, lower-case names, several query files) under different options: -# Ruby 3.1 (Struct) / 3.2 / generateGemfile / generateTypes=false. +# Snapshots of the plugin's output for the sqlc-captured queries plus synthetic ones, under different options: +# Ruby 3.1 (Struct) / 3.2 / generateGemfile / generateTypes=false. Review the diff when they change: +# UPDATE_GOLDEN=1 ruby -Itest test/extended_golden_test.rb class ExtendedGoldenTest < Minitest::Test include PluginHelper GOLDEN = File.join(PluginHelper::ROOT, 'test/golden') + VARIANTS = { + 'default' => {}, + 'ruby31_gemfile' => { rubyVersion: '3.1', generateGemfile: true }, + 'ruby32_notypes' => { rubyVersion: '3.2', generateTypes: false } + }.freeze - Dir.glob(File.join(GOLDEN, '*/*/')).sort.each do |dir| - driver_case = File.basename(File.dirname(dir)) - variant = File.basename(dir) - define_method("test_#{driver_case}_#{variant}") do - options = JSON.parse(File.read(File.join(dir, 'options.json'))) - files = generate(load_request(driver_case, **options.transform_keys(&:to_sym))) - expected = Dir.children(dir).reject { |f| f == 'options.json' }.sort - assert_equal expected, files.keys.sort - expected.each { |name| assert_equal File.binread(File.join(dir, name)), files.fetch(name), name } + %w[pg mysql2 sqlite3].product(VARIANTS.to_a).each do |driver, (variant, options)| + define_method("test_#{driver}_extended_#{variant}") do + dir = File.join(GOLDEN, driver, variant) + files = generate(SyntheticRequests.extended_request(driver, **options)) + if ENV['UPDATE_GOLDEN'] + FileUtils.rm_rf(dir) + FileUtils.mkdir_p(dir) + files.each { |name, contents| File.binwrite(File.join(dir, name), contents) } + end + assert_equal Dir.children(dir).sort, files.keys.sort + files.each { |name, contents| assert_equal File.binread(File.join(dir, name)), contents, "#{dir}/#{name}" } end end end diff --git a/test/features_test.rb b/test/features_test.rb new file mode 100644 index 0000000..3e44946 --- /dev/null +++ b/test/features_test.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +# Generator behaviour for the sqlc features the example schemas exercise (details of the runtime behaviour of the +# generated code are covered by the end-to-end tests under tests/). +class FeaturesTest < Minitest::Test + include PluginHelper + + def query(request, name) = request.queries.find { |q| q.name == name } + + def test_nullable_columns_get_optional_types + rbs = generate(load_request('pg')).fetch('query_sql.rbs') + assert_match(/class GetAuthorRow\n\s+attr_reader id: Integer\n\s+attr_reader name: String\n\s+attr_reader bio: String\?/, rbs) + end + + def test_overrides_replace_type_and_nullability + overrides = [{ column: 'GetAuthor:bio', ruby_type: { type: 'Symbol', notNull: true } }, + { column: '*:name', ruby_type: { type: 'Pathname', notNull: false } }] + rbs = generate(load_request('pg', overrides: overrides)).fetch('query_sql.rbs') + assert_match(/class GetAuthorRow\n.*\n\s+attr_reader name: Pathname\?\n\s+attr_reader bio: Symbol\n/, rbs) + assert_match(/class ListAuthorsRow\n.*\n\s+attr_reader name: Pathname\?\n\s+attr_reader bio: String\?\n/, rbs) + end + + def test_invalid_overrides_are_rejected + assert_raises(RuntimeError) { generate(load_request('pg', overrides: [{ column: 'nocolon', ruby_type: {} }])) } + end + + def test_array_parameters_and_columns + rbs = generate(load_request('pg')).fetch('query_sql.rbs') + assert_includes rbs, 'attr_reader param1: Array[Integer]' + assert_includes rbs, 'attr_reader c_text_array: Array[String]?' + end + + def test_unnamed_parameters_get_positional_names + files = generate(load_request('pg')) + assert_includes files.fetch('query_sql.rb'), 'class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2)' + end + + def test_models_cover_tables_and_enums + pg = generate(load_request('pg')) + assert_includes pg.fetch('models.rb'), 'class Author < Data.define(:id, :name, :bio)' + assert_includes pg.fetch('models.rb'), "AUTOBIOGRAPHY = \"Autobiography\"" + assert_includes pg.fetch('models.rb'), 'module ExtendedBioType' + assert_includes pg.fetch('models.rbs'), 'attr_reader bio_type: String?' + assert_includes pg.fetch('query_sql.rb'), "require_relative 'models'" + mysql = generate(load_request('mysql2')).fetch('models.rb') + assert_includes mysql, 'module ExtendedBiosBioType' + assert_includes mysql, 'module ExtendedBiosAuthorType' + end + + def test_models_fall_back_to_untyped_for_unknown_types + request = load_request('pg') + request.catalog.schemas.find { |s| s.name == 'public' }.tables.first.columns.first.type.name = 'geometry' + assert_includes generate(request).fetch('models.rbs'), 'attr_reader id: untyped' + end + + def test_unknown_types_in_queries_fail + request = load_request('pg') + query(request, 'GetAuthor').columns.first.type.name = 'geometry' + assert_raises(RuntimeError) { generate(request) } + end + + def test_embedded_tables_become_model_attributes + rb = generate(load_request('pg')).fetch('query_sql.rb') + assert_includes rb, 'class ListAllAuthorsBooksRow < Data.define(:author, :book)' + assert_includes rb, 'class GetDuplicateAuthorsRow < Data.define(:author, :author2)' + assert_includes rb, 'class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book)' + assert_includes rb, 'GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5]))' + end + + def test_slices_expand_at_runtime_and_are_array_typed + files = generate(load_request('mysql2')) + assert_includes files.fetch('query_sql.rb'), "sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids))" + assert_includes files.fetch('query_sql.rb'), 'query_params = [*get_authors_by_ids_args.ids]' + assert_includes files.fetch('query_sql.rbs'), 'attr_reader ids: Array[Integer]' + assert_includes files.fetch('query_sql.rbs'), 'def slice_placeholders: (Array[untyped]) -> String' + end + + def test_slices_are_rejected_for_postgres + request = load_request('pg') + query(request, 'GetAuthorsByIds').params.first.column.is_sqlc_slice = true + assert_raises(RuntimeError) { generate(request) } + end + + def test_command_return_types + rbs = generate(load_request('pg')).fetch('query_sql.rbs') + assert_includes rbs, 'def update_author_bios: (UpdateAuthorBiosArgs) -> Integer' + assert_includes rbs, 'def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer' + assert_includes rbs, 'def create_book: (CreateBookArgs) -> String' + assert_includes rbs, 'def copy_authors: (Array[CopyAuthorsArgs]) -> void' + assert_includes rbs, 'def list_authors: -> Array[ListAuthorsRow]' + assert_includes rbs, 'def transaction: [T] () { (self) -> T } -> T' + end + + def test_pg_and_sqlite_execlastid_need_returning + %w[pg sqlite3].each do |driver| + request = load_request(driver) + query(request, 'CreateBook').columns.clear + assert_raises(RuntimeError, driver) { generate(request) } + end + end + + def test_mysql_copyfrom_adds_csv_helpers_and_gemfile_entry + files = generate(load_request('mysql2', generateGemfile: true)) + assert_includes files.fetch('query_sql.rb'), "require 'csv'" + assert_includes files.fetch('query_sql.rb'), 'def csv_line(values)' + assert_includes files.fetch('Gemfile'), "gem 'csv'" + refute_includes generate(load_request('pg', generateGemfile: true)).fetch('Gemfile'), 'csv' + end + + def test_sqlite_and_mysql_close_prepared_statements + %w[mysql2 sqlite3].each do |driver| + assert_match(/ensure\n\t+stmt\.close\n/, generate(load_request(driver)).fetch('query_sql.rb'), driver) + end + end +end diff --git a/test/fixtures/mysql2.bin b/test/fixtures/mysql2.bin index 18c25b4..719bc29 100644 --- a/test/fixtures/mysql2.bin +++ b/test/fixtures/mysql2.bin @@ -1,8 +1,76 @@ +”public"‰public€ + authors& +id0ÿÿÿÿÿÿÿÿÿR authorsbbigint& +name0ÿÿÿÿÿÿÿÿÿR authorsbtext# +bio0ÿÿÿÿÿÿÿÿÿR authorsbtext÷ +node_mysql_types$ +c_bit0Rnode_mysql_typesbbit5 + c_tinyint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb tinyint) +c_bool0Rnode_mysql_typesb tinyint, + c_boolean0Rnode_mysql_typesb tinyint7 -ý -2mysql!examples/authors/mysql/schema.sql" examples/authors/mysql/query.sqlb« -e/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/out_mysql2rubyT{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"}*f -d/tmp/claude-0/-home-user-sqlc-gen-ruby/5ee7feff-4b4b-542d-a5a8-7a5a095e775a/scratchpad/cap/mysql2.shˆ +c_smallint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +smallint9 + c_mediumint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  mediumint- +c_int0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbint1 + c_integer0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbint3 +c_bigint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigint8 +c_serial0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigint€, + c_decimal0Rnode_mysql_typesb decimal( +c_dec0Rnode_mysql_typesb decimal, + c_numeric0Rnode_mysql_typesb decimal* +c_fixed0Rnode_mysql_typesb decimal1 +c_float0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbfloat3 +c_double0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdouble= +c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdouble/ +c_date0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdate& +c_time0 +Rnode_mysql_typesbtime. + +c_datetime0Rnode_mysql_typesb +datetime0 + c_timestamp0Rnode_mysql_typesb  timestamp/ +c_year0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbyear/ +c_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbchar0 +c_nchar0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbchar8 +c_national_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbchar, + c_varchar0 +Rnode_mysql_typesb varchar3 +c_binary0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbinary0 + c_varbinary0 +Rnode_mysql_typesb  varbinary7 + +c_tinyblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinyblob7 + +c_tinytext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +tinytext/ +c_blob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbblob/ +c_text0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbtext; + c_mediumblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumblob; + c_mediumtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  +mediumtext7 + +c_longblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longblob7 + +c_longtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb +longtext/ +c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjson­ +books$ +id0ÿÿÿÿÿÿÿÿÿRbooksbbigint$ +name0ÿÿÿÿÿÿÿÿÿRbooksbtext+ + author_id0ÿÿÿÿÿÿÿÿÿRbooksbbigint) + description0ÿÿÿÿÿÿÿÿÿRbooksbtextÜ + extended_bios+ + author_name0dR extended_biosb varchar$ +name0dR extended_biosb varchar7 +bio_type0 R extended_biosbextended_bios_bio_type= + author_type0R extended_biosbextended_bios_author_type": +extended_bios_bio_type Autobiography BiographyMemoir"7 +extended_bios_author_typeAuthorEditor +Translatorˆ 6SELECT id, name, bio FROM authors WHERE id = ? LIMIT 1 GetAuthor:one"* id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", @@ -97,4 +165,71 @@ c_longblob"C c_longtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb longtextz c_longtext"7 -c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjsonzc_json: query.sql"v1.27.0*T{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file +c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjsonzc_json: query.sqlµ +@SELECT id, name, bio FROM authors +ORDER BY name +LIMIT ? OFFSET ?ListAuthorsPaged:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*# +limit0ÿÿÿÿÿÿÿÿÿb integer*$ +offset0ÿÿÿÿÿÿÿÿÿb integer: query.sql¬ +BSELECT id, name, bio FROM authors +WHERE name LIKE COALESCE(?, '%')GetAuthorByNamePattern:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*84 + name_pattern0ÿÿÿÿÿÿÿÿÿ8R authorsbtextzname: query.sql’ +0UPDATE authors +SET bio = ? +WHERE bio IS NOT NULLUpdateAuthorBios :execrows*62 +bio0ÿÿÿÿÿÿÿÿÿ8Rpublicauthorsbtextzbio: query.sqlœ +>SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)GetAuthorsByIds:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*3/ +ids0ÿÿÿÿÿÿÿÿÿ8R authorsbbiginthzid: query.sqlú +]SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)GetAuthorsByIdsAndNames:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*3/ +ids0ÿÿÿÿÿÿÿÿÿ8R authorsbbiginthzid*51 +names0ÿÿÿÿÿÿÿÿÿ8R authorsbtexthzname: query.sqlÍ +-INSERT INTO authors (name, bio) VALUES (?, ?) CopyAuthors :copyfrom*84 +name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authors8 +DELETE FROM authorsTruncateAuthors:exec: query.sqlÜ +1INSERT INTO books (name, author_id) VALUES (?, ?) +CreateBook :execlastid*62 +name0ÿÿÿÿÿÿÿÿÿRpublicbooksbtextzname*B> + author_id0ÿÿÿÿÿÿÿÿÿRpublicbooksbbigintz author_id: query.sqlBbooks  +´SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description +FROM authors JOIN books ON authors.id = books.author_id +ORDER BY authors.nameListAllAuthorsBooks:many"! +authors0ÿÿÿÿÿÿÿÿÿbr authors" +books0ÿÿÿÿÿÿÿÿÿbrbooks: query.sql¶ +ÆSELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio +FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name +WHERE authors1.id < authors2.idGetDuplicateAuthors:many"! +authors0ÿÿÿÿÿÿÿÿÿbr authors"! +authors0ÿÿÿÿÿÿÿÿÿbr authors: query.sql± +³SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description +FROM authors JOIN books ON authors.id = books.author_id +WHERE books.name = ?GetAuthorsByBookName:many"* +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio" +books0ÿÿÿÿÿÿÿÿÿbrbooks*.* +name0ÿÿÿÿÿÿÿÿÿRbooksbtextzname: query.sqlµ +XINSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)CreateExtendedBio:exec*D@ + author_name0dRpublic extended_biosb varcharz author_name*62 +name0dRpublic extended_biosb varcharzname*MI +bio_type0 Rpublic extended_biosbextended_bios_bio_typezbio_type*VR + author_type0Rpublic extended_biosbextended_bios_author_typez author_type: query.sqlB extended_biosÇ +]SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1GetFirstExtendedBioByType:one"8 + author_name0dR extended_biosb varcharz author_name"* +name0dR extended_biosb varcharzname"A +bio_type0 R extended_biosbextended_bios_bio_typezbio_type"J + author_type0R extended_biosbextended_bios_author_typez author_type*EA +bio_type0 R extended_biosbextended_bios_bio_typezbio_type: query.sqlC +DELETE FROM extended_biosTruncateExtendedBios:exec: query.sql"v1.27.0*T{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/mysql2_extended.bin b/test/fixtures/mysql2_extended.bin deleted file mode 100644 index 9ab0eb5..0000000 --- a/test/fixtures/mysql2_extended.bin +++ /dev/null @@ -1,133 +0,0 @@ -ˆ -6SELECT id, name, bio FROM authors -WHERE id = ? LIMIT 1 GetAuthor:one"* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", -name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( -bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*.* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlÔ -/SELECT id, name, bio FROM authors -ORDER BY name ListAuthors:many"* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", -name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( -bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlÓ -6INSERT INTO authors ( - name, bio -) VALUES ( - ?, ? -) CreateAuthor:exec*84 -name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 -bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authors° -(UPDATE authors -SET bio = ? -WHERE id = ? UpdateAuthor:exec*40 -bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio*.* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlä -9INSERT INTO authors ( - name, bio -) VALUES ( - ?, ? -)CreateAuthorReturnId :execlastid*84 -name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 -bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsr - DELETE FROM authors -WHERE id = ? DeleteAuthor:exec*.* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sql´ -­SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types -LIMIT 1Test:one"+ -c_bit0Rnode_mysql_typesbbitzc_bit"@ - c_tinyint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb tinyintz c_tinyint"1 -c_bool0Rnode_mysql_typesb tinyintzc_bool"7 - c_boolean0Rnode_mysql_typesb tinyintz c_boolean"C - -c_smallint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb -smallintz -c_smallint"F - c_mediumint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  mediumintz c_mediumint"4 -c_int0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintzc_int"< - c_integer0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbintz c_integer"= -c_bigint0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_bigint"B -c_serial0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbigintzc_serial€"7 - c_decimal0Rnode_mysql_typesb decimalz c_decimal"/ -c_dec0Rnode_mysql_typesb decimalzc_dec"7 - c_numeric0Rnode_mysql_typesb decimalz c_numeric"3 -c_fixed0Rnode_mysql_typesb decimalzc_fixed": -c_float0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbfloatzc_float"= -c_double0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double"Q -c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdoublezc_double_precision"7 -c_date0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbdatezc_date". -c_time0 -Rnode_mysql_typesbtimezc_time": - -c_datetime0Rnode_mysql_typesb -datetimez -c_datetime"= - c_timestamp0Rnode_mysql_typesb  timestampz c_timestamp"7 -c_year0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbyearzc_year"7 -c_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_char"9 -c_nchar0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_nchar"I -c_national_char0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbcharzc_national_char"7 - c_varchar0 -Rnode_mysql_typesb varcharz c_varchar"= -c_binary0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbbinaryzc_binary"= - c_varbinary0 -Rnode_mysql_typesb  varbinaryz c_varbinary"C - -c_tinyblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb -tinyblobz -c_tinyblob"C - -c_tinytext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb -tinytextz -c_tinytext"7 -c_blob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbblobzc_blob"7 -c_text0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbtextzc_text"I - c_mediumblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  -mediumblobz c_mediumblob"I - c_mediumtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb  -mediumtextz c_mediumtext"C - -c_longblob0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb -longblobz -c_longblob"C - -c_longtext0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesb -longtextz -c_longtext"7 -c_json0ÿÿÿÿÿÿÿÿÿRnode_mysql_typesbjsonzc_json: query.sqlÊ -SELECT 1 -- ? ? - WHERE a = ? -ManyParams:one" -c0bint" -c1b varchar* -c0bint* -c1b varchar* -c2bdouble* -c3bblob* -c4b tinyint: query.sql¦ -SELECT 1 -- ? ? - WHERE a = ?manyRows:many" -c0bint" -c1b varchar" -c2bdouble" -c3bblob" -c4b tinyint* -c0bint: query.sql€ -SELECT 1 -- ? ? - WHERE a = ?FourCols:many" -c0bint" -c1b varchar" -c2bdouble" -c3bblob: query.sql¥ -SELECT 1 -- ? ? - WHERE a = ?NoCols:exec* -c0bint* -c1b varchar* -c2bdouble* -c3bblob* -c4b tinyint: other.sqlJ -SELECT 1 -- ? ? - WHERE a = ?OtherOne:one" -c0bint: other.sqlZ -SELECT 1 -- ? ? - WHERE a = ?WithNumbers2Go:exec* -c0bint:my_queries.sql"v1.27.0*T{"driver":"mysql2","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/pg.bin b/test/fixtures/pg.bin index 2a907b7de4dbca24b1fd632987fd70c732683a0b..efa9ec7b3c12c1b3a9d066480a4202ad378de25d 100644 GIT binary patch literal 10951 zcmdT~&u<%99mj5dI4`Z*zGl1K5meLJZa1+)XwtL|rd?_~PU<#wx{kB#Y5_C0C+>jl z@y?9P+KAI$KpZMAASA9_IH1)^Tsgpv3l}6LkoXG_7bLFm{l52R{KlS%$4ys+O74uu z-|u(c`+R@A?~l-*P&(J`)mmm<`{#M@R{11B7L<9t=RUJ+hpv#M*|?Ip`5*6JqCEVy zwx}%B%%)@5rrx4A$V^9X8(bIW+#pa8W1r9OdB*y z7GMRI)iU%>j8%+?Kt?VCgc(i4o?ZknZqf%Ng*Y@{b{|{x4ocw4i?N23GA9(hjr(sH zbrWEW!T4GL!ud|G4fax}Op5sO@=_jO%QyiWqr?~+IW2Qeyag}uV*&s^{~JwQ?amkn=J64TOAF!KIw?^!SlBu zKrc27r*4~Fcf@XeHnCUb$1~)zvgC(T0Ls=yP`!nU$HmIfMG*V@x{X>wL~3!kU^$Ey z%>68wE4;m`+qT|U1Bc5o??jd#B@*-rIbW}e*g1`ear8F1xO0;CxPv%*c_ZLJi@pQ7 z1ImyzJ4V0=kJ%NHQif#P>KOevZwDk9E&VDUW*y6|c6zN=@Z3G-jfkW6B1uzlH98HW zq5L^PE=XTz;COauo-7J`96!~^sB009d@s!OV{_jpDOd_KQ5lzU{v~PkK8v(hmte>?ZYXj`VOyhxI}2Zg;tzM`6qRTVeAsse|)Q{I1~ihPKC<6)s( z00Q;CdVO8pD?TY!)OEVBYq;~5luv{vZc7vuu z&*mW&agEbx3R_?bn_*^l(a-$7q#px31_^F%>CIG<-W)TzW7yF2B(!rw%U7P0Yun&s zmBKMzMbB;)i`)3 z5?{O(*(=^+3d(*RNhH}bFbTZkG2vZVzJ*^fQNpW8eejlLGmH?FfLA;>Ibb0L5%Gf1 zOa-zy5r6V0DUt@fz|u-5uayQ;wWaNyxNaH@3okBB=t@2vB5J{q(;uY?0otRJR#OAN z8q-*1Hho=%5;&K(|0*gh+@3I2F7Twq777$Hw1=25o}}&?2!8E5ud@Ccgw%3c2G=@W zO@^yO4s8c{(Q&6YG2}uyc-0PHRgE@~Ydx-pn6*}D!G;S&er_QrE!@yTgbeNe%hYfK zukPbY!&4{*@ZNcaU;_u|#h@9F7by`3yh`Vm^1CF-O_~3QU<^sZ{?5)pp#n)Fy&$DT zPxU6`+ovioO5cFYKqGV3(UD`gfhuAvQG>Hvgvj(;kOy2Nvn{i29z!Mr_-T^lCj-b+ zYD~HypMG3W^ZVJo!a=^UjOo_fZ@s-jFZ=0ru3rMWr@HGJcITAg$TO_7xRGj?uTZkT ztw7_{vsjkU%aHvnN$!ParhZa7%t?7Vx}wV2C|#0M(6Zt7>`t+vEV3l3r8_QE8`D(; z>C=%p{xm_>51(#lA$9a>G4wAi(6AH}g&BS=9;l`Lids6{+oKl_yBNlKLY{-NR#{+3 z*w*(QNNf`)N^F&%C&}02+BYj1v!xHfsxz5faTn?i^}}zi(DS}+JGp+bamtFObQ&C`!$f()UHY>$IzYMpHOr}zJpQHBr2x{3*0|UeVF?#8% z5MGs0q(z(lwy5*|Nu(A^D2%^Tw_ z9l67b%~R~1RcqmE8DSu3ozlH#|E+e+S7JzJhh?!XPhD^N?q~1X407h!|z=LF+NO%ch($5-@?Uev(&-|v#tUj-{?*tQ1uR(-LheasQ34)PH*5W^sChuC-v3}y~v|&L5RtjmWb==16bt#R3nHbg-Q`ol%WH8 zh>pQyx{=31t%WRg254=g@3h|~Myuw6Rh0i8@Zo}seeD1<$YlU7Y zcA#U4c4X^lKi+oIy8v-MY^HqGpEiUJqP^KNwqr1(0$n9RS7CGJNs=I#VMC{eEhTo7 zejnVna@_EFl1Q&=56rJ2H&2_X8EQ-zOHUF$me6euM(5o4s%C1I)=BipdeUpID3_%u zDVf9|C=*mT$5l8w%%1cHKnYn;g?I%O&T|!x4Y(&s0u?eIR3W=bzneA0u81)YiQ*pf46P9D6;VPwHH3SE@|e6ia-N0RCZ*D`7CZ$c*V5*n zudi*~UAvin`U9*N#KY@&S delta 326 zcmX>e`a!&&tAUw|(MX7^AiuaIy(qP~Fh@!)wIVUMASbn0Ke4nVBfqFvA0eqhT?2jHjQreG-O}RJB3*x=ie%mN z)I8my(xgg#)6`V+wA8dTU6Uk}BwbSzqZHjl(?knhb0BG8X_{(oZkh;mLQ!H#az;U7 xihgopfqs5zNqj-N5DUlvsmWzpfhk%^TnRXBq};-SbiLw?&3v3fT$6LOq5+q_Z*%|v diff --git a/test/fixtures/pg_extended.bin b/test/fixtures/pg_extended.bin deleted file mode 100644 index a98a0a2..0000000 --- a/test/fixtures/pg_extended.bin +++ /dev/null @@ -1,98 +0,0 @@ -� -7SELECT id, name, bio FROM authors -WHERE id = $1 LIMIT 1 GetAuthor:one"- -id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", -name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( -bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*1- -id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql× -/SELECT id, name, bio FROM authors -ORDER BY name ListAuthors:many"- -id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", -name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( -bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio: query.sqlö -SINSERT INTO authors ( - name, bio -) VALUES ( - $1, $2 -) -RETURNING id, name, bio CreateAuthor:one"- -id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid", -name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( -bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*84 -name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 -bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authorsv -!DELETE FROM authors -WHERE id = $1 DeleteAuthor:exec*1- -id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserialzid: query.sql² -ÞSELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types -LIMIT 1Test:one"C -c_bit0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogbitzc_bit"N - -c_smallint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogint2z -c_smallint"L - c_boolean0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogboolz c_boolean"L - c_integer0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogint4z c_integer"J -c_bigint0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogint8zc_bigint"@ -c_serial0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbserialzc_serial"O - c_decimal0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalognumericz c_decimal"O - c_numeric0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalognumericz c_numeric"H -c_real0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogfloat4zc_real"` -c_double_precision0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogfloat8zc_double_precision": -c_date0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbdatezc_date"F -c_time0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogtimezc_time"U - c_timestamp0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalog timestampz c_timestamp"H -c_char0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogbpcharzc_char"O - c_varchar0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesb -pg_catalogvarcharz c_varchar"= -c_bytea0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbbyteazc_bytea": -c_text0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbtextzc_text": -c_json0ÿÿÿÿÿÿÿÿÿRnode_postgres_typesbjsonzc_json: query.sqlÄ -SELECT 1 -- ? ? - WHERE a = ? -ManyParams:one" -c0bbool" -c1bint4* -c0bbool* -c1bint4* -c2btext* -c3bfloat8* -c4bbytea: query.sql£ -SELECT 1 -- ? ? - WHERE a = ?manyRows:many" -c0bbool" -c1bint4" -c2btext" -c3bfloat8" -c4bbytea* -c0bbool: query.sql~ -SELECT 1 -- ? ? - WHERE a = ?FourCols:many" -c0bbool" -c1bint4" -c2btext" -c3bfloat8: query.sql¡ -SELECT 1 -- ? ? - WHERE a = ?NoCols:exec* -c0bbool* -c1bint4* -c2btext* -c3bfloat8* -c4bbytea: other.sqlK -SELECT 1 -- ? ? - WHERE a = ?OtherOne:one" -c0bbool: other.sql[ -SELECT 1 -- ? ? - WHERE a = ?WithNumbers2Go:exec* -c0bbool:my_queries.sql"v1.27.0*P{"driver":"pg","generateGemfile":false,"generateTypes":true,"rubyVersion":"3.3"} \ No newline at end of file diff --git a/test/fixtures/sqlite3.bin b/test/fixtures/sqlite3.bin index 6873373f74662c2ccbbdb1df28da3b324eebabb8..13c71d9086972539b2c2ce014d3317aa03849843 100644 GIT binary patch literal 4576 zcmds*&2Af26vr7SX_}j$RyP%(k&tc&(!_&tVy7*UMX~%b2`=t9vd0A$sIudmbc8+Q z&WuYP#DXo0O1uD%058x5AhCc2J2pHE5}Z4CKJGY9(kKF1WHlZi-;dup=YP%#{w0Oy z2OZN=UIY*F*AiNi7ds>GvF*5c9i`0P8`9R_;h)ruzwN?fO*gbXt%+};d8;#^ruZ&Y&fJvqzJ+ zicEz&rO|Sax(ALq^i10l&m36?3(Mc6(A}0+*D3}vdl|w|WC&c8>^2(@NI(;P{Gry= zz<}H%MN+Rls2L=Om-eVv@^Q=a1>2&^J11{EmH9EaM#)UOw_qM1J@&n$Y`g^hW9A@r z^Qwd|a*)E(lM!{sS@%g_{zF2W=fh|;t6GzkKlNeZ<+|wx7+iUwFz8t0nE)Z)>vsv= zsp&1PX%GneM(9?ufk^CelqL^L^|sdXUl%i^h|+ka;?Ryq12-=e=rekt-UY<@Y3{qm zb27@pZo-Lb{%Q(mPdM__A2D|dwuLO2Bs=A?4tDz;&!dht zQxtcA;%&6TuJLHd&nKwxPNEhiazq67JcSBVvo;dxIAdA5rb$yX+D*Nt?}1d~8)E7< zsW)=0T2Ef~Q@G!8J<#Bb=;RD!{c{OzwfCzfNc*rbv7BPn3hGi&q}Zogi|7r5=TSTYh34R_6G||>mwGI*$}WcRUn%t2psPMpOA*)7GBox{1x=r9d7~B^ zs%0=_u(Sx0Y09wiYS1^4AigI{1R*Zj^cxFNm zNJ2f$vKb)I)~pD~&4R9Gd{ZBvZ^)mx8Gnvw!VJNNpsX;4AaSCAY}TQPzTbeX_AN<6 zk2dz_jEMzN)OCx;!>fKX)$f=1*jHwohMmAo6i$Oe?d`te#C@dlCgRYO1|cPZocvn~ z{W%3#P9&B~BFn|hf-~nu>wMBW&#aS-O05t{4hg)1#Yp@$w3iV*eh4Ut(VomfL2(S; zu~U=xV+-gORzXLrqha4n=8`|oyadSeO+=VK&Y_>Cz!d2u;lw0)Sg(+8`HZKvIkZ7b=TSbJ_fR*)6sGrP6cE zr_@mj3e3=^N4J%XazriafGY3N!J*lwO5w26cWM0I7!Rqd6g+1HFP%|${E#{>{I3EQ K+0Je&U;P8bK7nrl delta 346 zcmaE$JdvxOtC^XL(MX7`xG*QPBvnc&wIVUMASbn0Ke4nVBfqFvA0nY&oSc!Go2Uom zDJkMoQ&^f>R0&d&w2p}@O}`|!KtDMru{0%B*Fax4BR@A)x3oC5NEfIeSvNg3Pq(Nv z$)i%=G&R*cEj2Ao*Cfd#N!QfGC`C8XG|@uW97q~inx>kYn void + end + + class NodeMysqlType + attr_reader c_bit: Array[Integer]? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: String? + attr_reader c_dec: String? + attr_reader c_numeric: String? + attr_reader c_fixed: String? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_datetime: String? + attr_reader c_timestamp: String? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: Array[Integer]? + attr_reader c_varbinary: Array[Integer]? + attr_reader c_tinyblob: Array[Integer]? + attr_reader c_tinytext: String? + attr_reader c_blob: Array[Integer]? + attr_reader c_text: String? + attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumtext: String? + attr_reader c_longblob: Array[Integer]? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end + + class ExtendedBio + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2/default/my_queries_sql.rb similarity index 63% rename from test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb rename to test/golden/mysql2/default/my_queries_sql.rb index ed1a6a3..ed5335d 100644 --- a/test/golden/mysql2_extended/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/mysql2/default/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'models' module Mysql2Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -16,11 +17,29 @@ def initialize(connection_pool_params, mysql2_params) } end + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end diff --git a/test/golden/mysql2_extended/default/my_queries_sql.rbs b/test/golden/mysql2/default/my_queries_sql.rbs similarity index 87% rename from test/golden/mysql2_extended/default/my_queries_sql.rbs rename to test/golden/mysql2/default/my_queries_sql.rbs index fb8bee0..49ed0c8 100644 --- a/test/golden/mysql2_extended/default/my_queries_sql.rbs +++ b/test/golden/mysql2/default/my_queries_sql.rbs @@ -11,6 +11,8 @@ module Mysql2Codegen @db: untyped def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/mysql2/default/other_sql.rb b/test/golden/mysql2/default/other_sql.rb new file mode 100644 index 0000000..ffcdc25 --- /dev/null +++ b/test/golden/mysql2/default/other_sql.rb @@ -0,0 +1,98 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'models' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + ensure + stmt.close + end + end + end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/other_sql.rbs b/test/golden/mysql2/default/other_sql.rbs similarity index 75% rename from test/golden/mysql2_extended/default/other_sql.rbs rename to test/golden/mysql2/default/other_sql.rbs index 1f60863..a95ea3c 100644 --- a/test/golden/mysql2_extended/default/other_sql.rbs +++ b/test/golden/mysql2/default/other_sql.rbs @@ -24,12 +24,23 @@ module Mysql2Codegen def initialize: ( Integer c0) -> void end + CountRowsSql: String + + class CountRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + class OtherSql @db: untyped def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/mysql2/default/query_sql.rb b/test/golden/mysql2/default/query_sql.rb new file mode 100644 index 0000000..c74b768 --- /dev/null +++ b/test/golden/mysql2/default/query_sql.rb @@ -0,0 +1,669 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require 'csv' +require 'tempfile' +require_relative 'models' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Data.define(:bio, :id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ?) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:limit, :offset) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + ensure + stmt.close + end + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + ensure + stmt.close + end + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + return if copy_authors_args.empty? + Tempfile.create(['copy_authors', '.csv']) do |file| + copy_authors_args.each do |item| + file.write(csv_line([item.name, item.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + end + end + + def truncate_authors + @db.with do |client| + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + @db.with do |client| + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def get_duplicate_authors + @db.with do |client| + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params, as: :array) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] + stmt = client.prepare(CreateExtendedBioSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + stmt = client.prepare(GetFirstExtendedBioByTypeSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + return entity + ensure + stmt.close + end + end + end + + def truncate_extended_bios + @db.with do |client| + stmt = client.prepare(TruncateExtendedBiosSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + ensure + stmt.close + end + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + ensure + stmt.close + end + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + ensure + stmt.close + end + end + end + + private + + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2/default/query_sql.rbs b/test/golden/mysql2/default/query_sql.rbs new file mode 100644 index 0000000..fd46044 --- /dev/null +++ b/test/golden/mysql2/default/query_sql.rbs @@ -0,0 +1,390 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + UpdateAuthorSql: String + + class UpdateAuthorArgs + attr_reader bio: String? + attr_reader id: Integer + def initialize: ( String? bio, Integer id) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + TestSql: String + + class TestRow + attr_reader c_bit: Array[Integer]? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: String? + attr_reader c_dec: String? + attr_reader c_numeric: String? + attr_reader c_fixed: String? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_datetime: String? + attr_reader c_timestamp: String? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: Array[Integer]? + attr_reader c_varbinary: Array[Integer]? + attr_reader c_tinyblob: Array[Integer]? + attr_reader c_tinytext: String? + attr_reader c_blob: Array[Integer]? + attr_reader c_text: String? + attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumtext: String? + attr_reader c_longblob: Array[Integer]? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader limit: Integer + attr_reader offset: Integer + def initialize: ( Integer limit, Integer offset) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader ids: Array[Integer] + def initialize: ( Array[Integer] ids) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader ids: Array[Integer] + attr_reader names: Array[String] + def initialize: ( Array[Integer] ids, Array[String] names) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + class GetFirstExtendedBioByTypeArgs + attr_reader bio_type: String? + def initialize: ( String? bio_type) -> void + end + + TruncateExtendedBiosSql: String + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + class ManyParamsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + end + + class QuerySql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> void + + def update_author: (UpdateAuthorArgs) -> void + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + def csv_line: (Array[untyped]) -> String + def slice_placeholders: (Array[untyped]) -> String + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/Gemfile b/test/golden/mysql2/ruby31_gemfile/Gemfile similarity index 70% rename from test/golden/mysql2_extended/ruby31_gemfile/Gemfile rename to test/golden/mysql2/ruby31_gemfile/Gemfile index e0ff304..2fe5953 100644 --- a/test/golden/mysql2_extended/ruby31_gemfile/Gemfile +++ b/test/golden/mysql2/ruby31_gemfile/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' gem 'connection_pool' -gem 'mysql2' \ No newline at end of file +gem 'mysql2' +gem 'csv' \ No newline at end of file diff --git a/test/golden/mysql2/ruby31_gemfile/models.rb b/test/golden/mysql2/ruby31_gemfile/models.rb new file mode 100644 index 0000000..32b303b --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/models.rb @@ -0,0 +1,67 @@ +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + module ExtendedBiosBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = "Author" + EDITOR = "Editor" + TRANSLATOR = "Translator" + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Struct.new(id, name, bio) + end + + class NodeMysqlType < Struct.new( + c_bit, + c_tinyint, + c_bool, + c_boolean, + c_smallint, + c_mediumint, + c_int, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_dec, + c_numeric, + c_fixed, + c_float, + c_double, + c_double_precision, + c_date, + c_time, + c_datetime, + c_timestamp, + c_year, + c_char, + c_nchar, + c_national_char, + c_varchar, + c_binary, + c_varbinary, + c_tinyblob, + c_tinytext, + c_blob, + c_text, + c_mediumblob, + c_mediumtext, + c_longblob, + c_longtext, + c_json + ) + end + + class Book < Struct.new(id, name, author_id, description) + end + + class ExtendedBio < Struct.new(author_name, name, bio_type, author_type) + end +end \ No newline at end of file diff --git a/test/golden/mysql2/ruby31_gemfile/models.rbs b/test/golden/mysql2/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..863f1fa --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/models.rbs @@ -0,0 +1,118 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + module ExtendedBiosBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + module ExtendedBiosAuthorType + AUTHOR: String + EDITOR: String + TRANSLATOR: String + ALL: Array[String] + end + + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class NodeMysqlType < ::Struct + attr_accessor c_bit: Array[Integer]? + attr_accessor c_tinyint: Integer? + attr_accessor c_bool: Integer? + attr_accessor c_boolean: Integer? + attr_accessor c_smallint: Integer? + attr_accessor c_mediumint: Integer? + attr_accessor c_int: Integer? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer + attr_accessor c_decimal: String? + attr_accessor c_dec: String? + attr_accessor c_numeric: String? + attr_accessor c_fixed: String? + attr_accessor c_float: Float? + attr_accessor c_double: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: String? + attr_accessor c_time: String? + attr_accessor c_datetime: String? + attr_accessor c_timestamp: String? + attr_accessor c_year: Integer? + attr_accessor c_char: String? + attr_accessor c_nchar: String? + attr_accessor c_national_char: String? + attr_accessor c_varchar: String? + attr_accessor c_binary: Array[Integer]? + attr_accessor c_varbinary: Array[Integer]? + attr_accessor c_tinyblob: Array[Integer]? + attr_accessor c_tinytext: String? + attr_accessor c_blob: Array[Integer]? + attr_accessor c_text: String? + attr_accessor c_mediumblob: Array[Integer]? + attr_accessor c_mediumtext: String? + attr_accessor c_longblob: Array[Integer]? + attr_accessor c_longtext: String? + attr_accessor c_json: String? + def self.new: ( + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json + ) -> NodeMysqlType + end + + class Book < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book + end + + class ExtendedBio < ::Struct + attr_accessor author_name: String? + attr_accessor name: String? + attr_accessor bio_type: String? + attr_accessor author_type: String? + def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> ExtendedBio + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb similarity index 63% rename from test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb rename to test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb index 1155fae..9af655d 100644 --- a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'models' module Mysql2Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -16,11 +17,29 @@ def initialize(connection_pool_params, mysql2_params) } end + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end diff --git a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs similarity index 88% rename from test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs rename to test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs index f3f0844..e2e2e12 100644 --- a/test/golden/mysql2_extended/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs @@ -11,6 +11,8 @@ module Mysql2Codegen @db: untyped def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rb b/test/golden/mysql2/ruby31_gemfile/other_sql.rb new file mode 100644 index 0000000..cfb13a0 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rb @@ -0,0 +1,98 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'models' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Struct.new(c0) + end + + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Struct.new(c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + ensure + stmt.close + end + end + end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs similarity index 74% rename from test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs rename to test/golden/mysql2/ruby31_gemfile/other_sql.rbs index a104bdb..b4a71bd 100644 --- a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs @@ -24,12 +24,23 @@ module Mysql2Codegen def self.new: ( Integer c0) -> OtherOneRow end + CountRowsSql: String + + class CountRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> CountRowsArgs + end + class OtherSql @db: untyped def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rb b/test/golden/mysql2/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..5b82150 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rb @@ -0,0 +1,669 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require 'csv' +require 'tempfile' +require_relative 'models' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Struct.new(name, bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Struct.new(bio, id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Struct.new(id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Struct.new( + c_bit, + c_tinyint, + c_bool, + c_boolean, + c_smallint, + c_mediumint, + c_int, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_dec, + c_numeric, + c_fixed, + c_float, + c_double, + c_double_precision, + c_date, + c_time, + c_datetime, + c_timestamp, + c_year, + c_char, + c_nchar, + c_national_char, + c_varchar, + c_binary, + c_varbinary, + c_tinyblob, + c_tinytext, + c_blob, + c_text, + c_mediumblob, + c_mediumtext, + c_longblob, + c_longtext, + c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ?) + + class ListAuthorsPagedRow < Struct.new(id, name, bio) + end + + class ListAuthorsPagedArgs < Struct.new(limit, offset) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%')) + + class GetAuthorByNamePatternRow < Struct.new(id, name, bio) + end + + class GetAuthorByNamePatternArgs < Struct.new(name_pattern) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Struct.new(bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsArgs < Struct.new(ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsAndNamesArgs < Struct.new(ids, names) + end + + class CopyAuthorsArgs < Struct.new(name, bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) + + class CreateBookArgs < Struct.new(name, author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Struct.new(author, book) + end + + GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Struct.new(author, author2) + end + + GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) + end + + class GetAuthorsByBookNameArgs < Struct.new(name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) + + class CreateExtendedBioArgs < Struct.new(author_name, name, bio_type, author_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Struct.new(author_name, name, bio_type, author_type) + end + + class GetFirstExtendedBioByTypeArgs < Struct.new(bio_type) + end + + TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + ensure + stmt.close + end + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + ensure + stmt.close + end + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + return if copy_authors_args.empty? + Tempfile.create(['copy_authors', '.csv']) do |file| + copy_authors_args.each do |item| + file.write(csv_line([item.name, item.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + end + end + + def truncate_authors + @db.with do |client| + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + @db.with do |client| + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def get_duplicate_authors + @db.with do |client| + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params, as: :array) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] + stmt = client.prepare(CreateExtendedBioSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + stmt = client.prepare(GetFirstExtendedBioByTypeSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + return entity + ensure + stmt.close + end + end + end + + def truncate_extended_bios + @db.with do |client| + stmt = client.prepare(TruncateExtendedBiosSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + ensure + stmt.close + end + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + ensure + stmt.close + end + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + ensure + stmt.close + end + end + end + + private + + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..de5a5da --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,390 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorArgs + end + + UpdateAuthorSql: String + + class UpdateAuthorArgs < ::Struct + attr_accessor bio: String? + attr_accessor id: Integer + def self.new: ( String? bio, Integer id) -> UpdateAuthorArgs + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + TestSql: String + + class TestRow < ::Struct + attr_accessor c_bit: Array[Integer]? + attr_accessor c_tinyint: Integer? + attr_accessor c_bool: Integer? + attr_accessor c_boolean: Integer? + attr_accessor c_smallint: Integer? + attr_accessor c_mediumint: Integer? + attr_accessor c_int: Integer? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer + attr_accessor c_decimal: String? + attr_accessor c_dec: String? + attr_accessor c_numeric: String? + attr_accessor c_fixed: String? + attr_accessor c_float: Float? + attr_accessor c_double: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: String? + attr_accessor c_time: String? + attr_accessor c_datetime: String? + attr_accessor c_timestamp: String? + attr_accessor c_year: Integer? + attr_accessor c_char: String? + attr_accessor c_nchar: String? + attr_accessor c_national_char: String? + attr_accessor c_varchar: String? + attr_accessor c_binary: Array[Integer]? + attr_accessor c_varbinary: Array[Integer]? + attr_accessor c_tinyblob: Array[Integer]? + attr_accessor c_tinytext: String? + attr_accessor c_blob: Array[Integer]? + attr_accessor c_text: String? + attr_accessor c_mediumblob: Array[Integer]? + attr_accessor c_mediumtext: String? + attr_accessor c_longblob: Array[Integer]? + attr_accessor c_longtext: String? + attr_accessor c_json: String? + def self.new: ( + Array[Integer]? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + String? c_decimal, + String? c_dec, + String? c_numeric, + String? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_datetime, + String? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + Array[Integer]? c_binary, + Array[Integer]? c_varbinary, + Array[Integer]? c_tinyblob, + String? c_tinytext, + Array[Integer]? c_blob, + String? c_text, + Array[Integer]? c_mediumblob, + String? c_mediumtext, + Array[Integer]? c_longblob, + String? c_longtext, + String? c_json + ) -> TestRow + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + class ListAuthorsPagedArgs < ::Struct + attr_accessor limit: Integer + attr_accessor offset: Integer + def self.new: ( Integer limit, Integer offset) -> ListAuthorsPagedArgs + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + class GetAuthorByNamePatternArgs < ::Struct + attr_accessor name_pattern: String? + def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs < ::Struct + attr_accessor bio: String? + def self.new: ( String? bio) -> UpdateAuthorBiosArgs + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + class GetAuthorsByIdsArgs < ::Struct + attr_accessor ids: Array[Integer] + def self.new: ( Array[Integer] ids) -> GetAuthorsByIdsArgs + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class GetAuthorsByIdsAndNamesArgs < ::Struct + attr_accessor ids: Array[Integer] + attr_accessor names: Array[String] + def self.new: ( Array[Integer] ids, Array[String] names) -> GetAuthorsByIdsAndNamesArgs + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookArgs < ::Struct + attr_accessor name: String + attr_accessor author_id: Integer + def self.new: ( String name, Integer author_id) -> CreateBookArgs + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + class GetAuthorsByBookNameArgs < ::Struct + attr_accessor name: String + def self.new: ( String name) -> GetAuthorsByBookNameArgs + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs < ::Struct + attr_accessor author_name: String? + attr_accessor name: String? + attr_accessor bio_type: String? + attr_accessor author_type: String? + def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> CreateExtendedBioArgs + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow < ::Struct + attr_accessor author_name: String? + attr_accessor name: String? + attr_accessor bio_type: String? + attr_accessor author_type: String? + def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> GetFirstExtendedBioByTypeRow + end + + class GetFirstExtendedBioByTypeArgs < ::Struct + attr_accessor bio_type: String? + def self.new: ( String? bio_type) -> GetFirstExtendedBioByTypeArgs + end + + TruncateExtendedBiosSql: String + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + Integer c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + end + + class QuerySql + @db: untyped + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> void + + def update_author: (UpdateAuthorArgs) -> void + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + def csv_line: (Array[untyped]) -> String + def slice_placeholders: (Array[untyped]) -> String + end +end \ No newline at end of file diff --git a/test/golden/mysql2/ruby32_notypes/models.rb b/test/golden/mysql2/ruby32_notypes/models.rb new file mode 100644 index 0000000..627f01b --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/models.rb @@ -0,0 +1,67 @@ +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + module ExtendedBiosBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = "Author" + EDITOR = "Editor" + TRANSLATOR = "Translator" + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodeMysqlType < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/my_queries_sql.rb b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb similarity index 63% rename from test/golden/mysql2_extended/default/my_queries_sql.rb rename to test/golden/mysql2/ruby32_notypes/my_queries_sql.rb index ed1a6a3..ed5335d 100644 --- a/test/golden/mysql2_extended/default/my_queries_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'models' module Mysql2Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -16,11 +17,29 @@ def initialize(connection_pool_params, mysql2_params) } end + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end diff --git a/test/golden/mysql2/ruby32_notypes/other_sql.rb b/test/golden/mysql2/ruby32_notypes/other_sql.rb new file mode 100644 index 0000000..ffcdc25 --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/other_sql.rb @@ -0,0 +1,98 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'models' + +module Mysql2Codegen + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class NoColsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class OtherOneRow < Data.define(:c0) + end + + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + + class OtherSql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def no_cols(no_cols_args) + @db.with do |client| + query_params = [ + no_cols_args.c0, + no_cols_args.c1, + no_cols_args.c2, + no_cols_args.c3, + no_cols_args.c4 + ] + stmt = client.prepare(NoColsSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def other_one + @db.with do |client| + stmt = client.prepare(OtherOneSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row['c0']) + return entity + ensure + stmt.close + end + end + end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2/ruby32_notypes/query_sql.rb b/test/golden/mysql2/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..c74b768 --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/query_sql.rb @@ -0,0 +1,669 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require 'csv' +require 'tempfile' +require_relative 'models' + +module Mysql2Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + UpdateAuthorSql = %q(UPDATE authors + SET bio = ? + WHERE id = ?) + + class UpdateAuthorArgs < Data.define(:bio, :id) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ?) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:limit, :offset) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, mysql2_params) + @db = ConnectionPool.new(**connection_pool_params) { + Mysql2::Client.new(**mysql2_params) + } + end + + def transaction + @db.with do |client| + client.query('BEGIN') + begin + result = yield self + client.query('COMMIT') + result + rescue Exception + client.query('ROLLBACK') + raise + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + ensure + stmt.close + end + end + end + + def list_authors + @db.with do |client| + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def update_author(update_author_args) + @db.with do |client| + query_params = [update_author_args.bio, update_author_args.id] + stmt = client.prepare(UpdateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def test + @db.with do |client| + stmt = client.prepare(TestSql) + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + return entity + ensure + stmt.close + end + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + ensure + stmt.close + end + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + return if copy_authors_args.empty? + Tempfile.create(['copy_authors', '.csv']) do |file| + copy_authors_args.each do |item| + file.write(csv_line([item.name, item.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + end + end + + def truncate_authors + @db.with do |client| + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + stmt.execute(*query_params) + return client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + @db.with do |client| + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def get_duplicate_authors + @db.with do |client| + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute(as: :array) + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params, as: :array) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] + stmt = client.prepare(CreateExtendedBioSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + stmt = client.prepare(GetFirstExtendedBioByTypeSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + return entity + ensure + stmt.close + end + end + end + + def truncate_extended_bios + @db.with do |client| + stmt = client.prepare(TruncateExtendedBiosSql) + begin + stmt.execute + ensure + stmt.close + end + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + ensure + stmt.close + end + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + ensure + stmt.close + end + end + end + + def four_cols + @db.with do |client| + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + ensure + stmt.close + end + end + end + + private + + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/options.json b/test/golden/mysql2_extended/default/options.json deleted file mode 100644 index 2c63c08..0000000 --- a/test/golden/mysql2_extended/default/options.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/test/golden/mysql2_extended/default/other_sql.rb b/test/golden/mysql2_extended/default/other_sql.rb deleted file mode 100644 index 5853c6b..0000000 --- a/test/golden/mysql2_extended/default/other_sql.rb +++ /dev/null @@ -1,56 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/query_sql.rb b/test/golden/mysql2_extended/default/query_sql.rb deleted file mode 100644 index d7d0d13..0000000 --- a/test/golden/mysql2_extended/default/query_sql.rb +++ /dev/null @@ -1,294 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Data.define(:bio, :id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - stmt.execute(*query_params) - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - stmt.execute(*query_params) - return client.last_id - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/default/query_sql.rbs b/test/golden/mysql2_extended/default/query_sql.rbs deleted file mode 100644 index 2f3f7e2..0000000 --- a/test/golden/mysql2_extended/default/query_sql.rbs +++ /dev/null @@ -1,217 +0,0 @@ -# auto-generated by sqlc - do not edit -module Mysql2Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void - end - - UpdateAuthorSql: String - - class UpdateAuthorArgs - attr_reader bio: String - attr_reader id: Integer - def initialize: ( String bio, Integer id) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: Array[Integer] - attr_reader c_tinyint: Integer - attr_reader c_bool: Integer - attr_reader c_boolean: Integer - attr_reader c_smallint: Integer - attr_reader c_mediumint: Integer - attr_reader c_int: Integer - attr_reader c_integer: Integer - attr_reader c_bigint: Integer - attr_reader c_serial: Integer - attr_reader c_decimal: String - attr_reader c_dec: String - attr_reader c_numeric: String - attr_reader c_fixed: String - attr_reader c_float: Float - attr_reader c_double: Float - attr_reader c_double_precision: Float - attr_reader c_date: String - attr_reader c_time: String - attr_reader c_datetime: String - attr_reader c_timestamp: String - attr_reader c_year: Integer - attr_reader c_char: String - attr_reader c_nchar: String - attr_reader c_national_char: String - attr_reader c_varchar: String - attr_reader c_binary: Array[Integer] - attr_reader c_varbinary: Array[Integer] - attr_reader c_tinyblob: Array[Integer] - attr_reader c_tinytext: String - attr_reader c_blob: Array[Integer] - attr_reader c_text: String - attr_reader c_mediumblob: Array[Integer] - attr_reader c_mediumtext: String - attr_reader c_longblob: Array[Integer] - attr_reader c_longtext: String - attr_reader c_json: String - def initialize: ( - Array[Integer] c_bit, - Integer c_tinyint, - Integer c_bool, - Integer c_boolean, - Integer c_smallint, - Integer c_mediumint, - Integer c_int, - Integer c_integer, - Integer c_bigint, - Integer c_serial, - String c_decimal, - String c_dec, - String c_numeric, - String c_fixed, - Float c_float, - Float c_double, - Float c_double_precision, - String c_date, - String c_time, - String c_datetime, - String c_timestamp, - Integer c_year, - String c_char, - String c_nchar, - String c_national_char, - String c_varchar, - Array[Integer] c_binary, - Array[Integer] c_varbinary, - Array[Integer] c_tinyblob, - String c_tinytext, - Array[Integer] c_blob, - String c_text, - Array[Integer] c_mediumblob, - String c_mediumtext, - Array[Integer] c_longblob, - String c_longtext, - String c_json - ) -> void - end - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: Integer - attr_reader c1: String - def initialize: ( Integer c0, String c1) -> void - end - - class ManyParamsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - Integer c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - Integer c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void - end - - class QuerySql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> void - - def update_author: (UpdateAuthorArgs) -> void - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/options.json b/test/golden/mysql2_extended/ruby31_gemfile/options.json deleted file mode 100644 index 8e3677f..0000000 --- a/test/golden/mysql2_extended/ruby31_gemfile/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.1", - "generateGemfile": true -} diff --git a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb b/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb deleted file mode 100644 index 5e16504..0000000 --- a/test/golden/mysql2_extended/ruby31_gemfile/other_sql.rb +++ /dev/null @@ -1,56 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Struct.new(c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb deleted file mode 100644 index cf2dde0..0000000 --- a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rb +++ /dev/null @@ -1,294 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Struct.new(name, bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Struct.new(bio, id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Struct.new(id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Struct.new( - c_bit, - c_tinyint, - c_bool, - c_boolean, - c_smallint, - c_mediumint, - c_int, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_dec, - c_numeric, - c_fixed, - c_float, - c_double, - c_double_precision, - c_date, - c_time, - c_datetime, - c_timestamp, - c_year, - c_char, - c_nchar, - c_national_char, - c_varchar, - c_binary, - c_varbinary, - c_tinyblob, - c_tinytext, - c_blob, - c_text, - c_mediumblob, - c_mediumtext, - c_longblob, - c_longtext, - c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - stmt.execute(*query_params) - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - stmt.execute(*query_params) - return client.last_id - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs deleted file mode 100644 index 0603e1b..0000000 --- a/test/golden/mysql2_extended/ruby31_gemfile/query_sql.rbs +++ /dev/null @@ -1,217 +0,0 @@ -# auto-generated by sqlc - do not edit -module Mysql2Codegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String - def self.new: ( String name, String bio) -> CreateAuthorArgs - end - - UpdateAuthorSql: String - - class UpdateAuthorArgs < ::Struct - attr_accessor bio: String - attr_accessor id: Integer - def self.new: ( String bio, Integer id) -> UpdateAuthorArgs - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String - def self.new: ( String name, String bio) -> CreateAuthorReturnIdArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - TestSql: String - - class TestRow < ::Struct - attr_accessor c_bit: Array[Integer] - attr_accessor c_tinyint: Integer - attr_accessor c_bool: Integer - attr_accessor c_boolean: Integer - attr_accessor c_smallint: Integer - attr_accessor c_mediumint: Integer - attr_accessor c_int: Integer - attr_accessor c_integer: Integer - attr_accessor c_bigint: Integer - attr_accessor c_serial: Integer - attr_accessor c_decimal: String - attr_accessor c_dec: String - attr_accessor c_numeric: String - attr_accessor c_fixed: String - attr_accessor c_float: Float - attr_accessor c_double: Float - attr_accessor c_double_precision: Float - attr_accessor c_date: String - attr_accessor c_time: String - attr_accessor c_datetime: String - attr_accessor c_timestamp: String - attr_accessor c_year: Integer - attr_accessor c_char: String - attr_accessor c_nchar: String - attr_accessor c_national_char: String - attr_accessor c_varchar: String - attr_accessor c_binary: Array[Integer] - attr_accessor c_varbinary: Array[Integer] - attr_accessor c_tinyblob: Array[Integer] - attr_accessor c_tinytext: String - attr_accessor c_blob: Array[Integer] - attr_accessor c_text: String - attr_accessor c_mediumblob: Array[Integer] - attr_accessor c_mediumtext: String - attr_accessor c_longblob: Array[Integer] - attr_accessor c_longtext: String - attr_accessor c_json: String - def self.new: ( - Array[Integer] c_bit, - Integer c_tinyint, - Integer c_bool, - Integer c_boolean, - Integer c_smallint, - Integer c_mediumint, - Integer c_int, - Integer c_integer, - Integer c_bigint, - Integer c_serial, - String c_decimal, - String c_dec, - String c_numeric, - String c_fixed, - Float c_float, - Float c_double, - Float c_double_precision, - String c_date, - String c_time, - String c_datetime, - String c_timestamp, - Integer c_year, - String c_char, - String c_nchar, - String c_national_char, - String c_varchar, - Array[Integer] c_binary, - Array[Integer] c_varbinary, - Array[Integer] c_tinyblob, - String c_tinytext, - Array[Integer] c_blob, - String c_text, - Array[Integer] c_mediumblob, - String c_mediumtext, - Array[Integer] c_longblob, - String c_longtext, - String c_json - ) -> TestRow - end - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - def self.new: ( Integer c0, String c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - Integer c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - Integer c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow - end - - class QuerySql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> void - - def update_author: (UpdateAuthorArgs) -> void - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/options.json b/test/golden/mysql2_extended/ruby32_notypes/options.json deleted file mode 100644 index ea1ba9b..0000000 --- a/test/golden/mysql2_extended/ruby32_notypes/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.2", - "generateTypes": false -} diff --git a/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb b/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb deleted file mode 100644 index 5853c6b..0000000 --- a/test/golden/mysql2_extended/ruby32_notypes/other_sql.rb +++ /dev/null @@ -1,56 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - end -end \ No newline at end of file diff --git a/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb b/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb deleted file mode 100644 index d7d0d13..0000000 --- a/test/golden/mysql2_extended/ruby32_notypes/query_sql.rb +++ /dev/null @@ -1,294 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'mysql2' - -module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Data.define(:bio, :id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - stmt.execute(*query_params) - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - stmt.execute(*query_params) - return client.last_id - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/pg/default/models.rb b/test/golden/pg/default/models.rb new file mode 100644 index 0000000..9539cb7 --- /dev/null +++ b/test/golden/pg/default/models.rb @@ -0,0 +1,53 @@ +# auto-generated by sqlc - do not edit + +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end \ No newline at end of file diff --git a/test/golden/pg/default/models.rbs b/test/golden/pg/default/models.rbs new file mode 100644 index 0000000..e7920f4 --- /dev/null +++ b/test/golden/pg/default/models.rbs @@ -0,0 +1,93 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodePostgresType + attr_reader c_bit: Array[Integer]? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: Float? + attr_reader c_numeric: Float? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_timestamp: String? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: Array[Integer]? + attr_reader c_text: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json + ) -> void + end + + class Book + attr_reader id: String + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( String id, String name, Integer author_id, String? description) -> void + end + + class PostgresExtraType + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + class ExtendedBio + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb similarity index 62% rename from test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb rename to test/golden/pg/default/my_queries_sql.rb index 46a3001..14da5d7 100644 --- a/test/golden/pg_extended/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class WithNumbers2GoArgs < Data.define(:c0) end @@ -14,12 +15,23 @@ class My_queriesSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] diff --git a/test/golden/pg_extended/default/my_queries_sql.rbs b/test/golden/pg/default/my_queries_sql.rbs similarity index 88% rename from test/golden/pg_extended/default/my_queries_sql.rbs rename to test/golden/pg/default/my_queries_sql.rbs index d746da3..d245c44 100644 --- a/test/golden/pg_extended/default/my_queries_sql.rbs +++ b/test/golden/pg/default/my_queries_sql.rbs @@ -12,6 +12,8 @@ module PgCodegen @prepared_statements: Set[String] def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/pg_extended/default/other_sql.rb b/test/golden/pg/default/other_sql.rb similarity index 56% rename from test/golden/pg_extended/default/other_sql.rb rename to test/golden/pg/default/other_sql.rb index 1681c08..33e656e 100644 --- a/test/golden/pg_extended/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class NoColsArgs < Data.define( :c0, @@ -16,22 +17,39 @@ class NoColsArgs < Data.define( ) end - OtherOneSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class OtherOneRow < Data.define(:c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + class OtherSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def no_cols(no_cols_args) @db.with do |client| query_params = [ @@ -62,5 +80,17 @@ def other_one return entity end end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + unless @prepared_statements.include?('count_rows') + client.prepare('count_rows', CountRowsSql) + @prepared_statements.add('count_rows') + end + result = client.exec_prepared('count_rows', query_params) + return result.cmd_tuples + end + end end end \ No newline at end of file diff --git a/test/golden/pg_extended/default/other_sql.rbs b/test/golden/pg/default/other_sql.rbs similarity index 76% rename from test/golden/pg_extended/default/other_sql.rbs rename to test/golden/pg/default/other_sql.rbs index 73b58f0..2a7bc6f 100644 --- a/test/golden/pg_extended/default/other_sql.rbs +++ b/test/golden/pg/default/other_sql.rbs @@ -24,13 +24,24 @@ module PgCodegen def initialize: ( bool c0) -> void end + CountRowsSql: String + + class CountRowsArgs + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + class OtherSql @db: untyped @prepared_statements: Set[String] def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb new file mode 100644 index 0000000..7c250a0 --- /dev/null +++ b/test/golden/pg/default/query_sql.rb @@ -0,0 +1,671 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' +require_relative 'models' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT [])) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:param1) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) + + InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) + + class InsertExtraTypesArgs < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client + client + } + @prepared_statements = Set[] + end + + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + unless @prepared_statements.include?('list_authors_paged') + client.prepare('list_authors_paged', ListAuthorsPagedSql) + @prepared_statements.add('list_authors_paged') + end + result = client.exec_prepared('list_authors_paged', query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + unless @prepared_statements.include?('get_author_by_name_pattern') + client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) + @prepared_statements.add('get_author_by_name_pattern') + end + result = client.exec_prepared('get_author_by_name_pattern', query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + unless @prepared_statements.include?('create_author_return_id') + client.prepare('create_author_return_id', CreateAuthorReturnIdSql) + @prepared_statements.add('create_author_return_id') + end + result = client.exec_prepared('create_author_return_id', query_params) + return result.first['id'] + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + unless @prepared_statements.include?('update_author_bios') + client.prepare('update_author_bios', UpdateAuthorBiosSql) + @prepared_statements.add('update_author_bios') + end + result = client.exec_prepared('update_author_bios', query_params) + return result.cmd_tuples + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [get_authors_by_ids_args.param1] + unless @prepared_statements.include?('get_authors_by_ids') + client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) + @prepared_statements.add('get_authors_by_ids') + end + result = client.exec_prepared('get_authors_by_ids', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] + unless @prepared_statements.include?('get_authors_by_ids_and_names') + client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) + @prepared_statements.add('get_authors_by_ids_and_names') + end + result = client.exec_prepared('get_authors_by_ids_and_names', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + copy_authors_args.each do |item| + client.put_copy_data([item.name, item.bio]) + end + end + end + end + + def truncate_authors + @db.with do |client| + unless @prepared_statements.include?('truncate_authors') + client.prepare('truncate_authors', TruncateAuthorsSql) + @prepared_statements.add('truncate_authors') + end + client.exec_prepared('truncate_authors') + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + unless @prepared_statements.include?('create_book') + client.prepare('create_book', CreateBookSql) + @prepared_statements.add('create_book') + end + result = client.exec_prepared('create_book', query_params) + return result.first['id'] + end + end + + def list_all_authors_books + @db.with do |client| + unless @prepared_statements.include?('list_all_authors_books') + client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) + @prepared_statements.add('list_all_authors_books') + end + result = client.exec_prepared('list_all_authors_books').values + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def get_duplicate_authors + @db.with do |client| + unless @prepared_statements.include?('get_duplicate_authors') + client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) + @prepared_statements.add('get_duplicate_authors') + end + result = client.exec_prepared('get_duplicate_authors').values + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + unless @prepared_statements.include?('get_authors_by_book_name') + client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) + @prepared_statements.add('get_authors_by_book_name') + end + result = client.exec_prepared('get_authors_by_book_name', query_params).values + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] + unless @prepared_statements.include?('create_extended_bio') + client.prepare('create_extended_bio', CreateExtendedBioSql) + @prepared_statements.add('create_extended_bio') + end + client.exec_prepared('create_extended_bio', query_params) + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + unless @prepared_statements.include?('get_first_extended_bio_by_type') + client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) + @prepared_statements.add('get_first_extended_bio_by_type') + end + result = client.exec_prepared('get_first_extended_bio_by_type', query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + return entity + end + end + + def truncate_extended_bios + @db.with do |client| + unless @prepared_statements.include?('truncate_extended_bios') + client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) + @prepared_statements.add('truncate_extended_bios') + end + client.exec_prepared('truncate_extended_bios') + end + end + + def insert_extra_types(insert_extra_types_args) + @db.with do |client| + query_params = [ + insert_extra_types_args.c_uuid, + insert_extra_types_args.c_jsonb, + insert_extra_types_args.c_text_array, + insert_extra_types_args.c_integer_array, + insert_extra_types_args.c_timestamptz, + insert_extra_types_args.c_inet, + insert_extra_types_args.c_money, + insert_extra_types_args.c_not_null + ] + unless @prepared_statements.include?('insert_extra_types') + client.prepare('insert_extra_types', InsertExtraTypesSql) + @prepared_statements.add('insert_extra_types') + end + client.exec_prepared('insert_extra_types', query_params) + end + end + + def get_extra_types + @db.with do |client| + unless @prepared_statements.include?('get_extra_types') + client.prepare('get_extra_types', GetExtraTypesSql) + @prepared_statements.add('get_extra_types') + end + result = client.exec_prepared('get_extra_types') + row = result.first + return nil if row.nil? + entity = GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + return entity + end + end + + def truncate_extra_types + @db.with do |client| + unless @prepared_statements.include?('truncate_extra_types') + client.prepare('truncate_extra_types', TruncateExtraTypesSql) + @prepared_statements.add('truncate_extra_types') + end + client.exec_prepared('truncate_extra_types') + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs new file mode 100644 index 0000000..c6fed01 --- /dev/null +++ b/test/golden/pg/default/query_sql.rbs @@ -0,0 +1,408 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + TestSql: String + + class TestRow + attr_reader c_bit: Array[Integer]? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: Float? + attr_reader c_numeric: Float? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: String? + attr_reader c_time: String? + attr_reader c_timestamp: String? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: Array[Integer]? + attr_reader c_text: String? + attr_reader c_json: String? + def initialize: ( + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json + ) -> void + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader offset: Integer + attr_reader limit: Integer + def initialize: ( Integer offset, Integer limit) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader param1: Array[Integer] + def initialize: ( Array[Integer] param1) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader param1: Array[Integer] + attr_reader param2: Array[String] + def initialize: ( Array[Integer] param1, Array[String] param2) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow + attr_reader id: String + def initialize: ( String id) -> void + end + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + class GetFirstExtendedBioByTypeArgs + attr_reader bio_type: String? + def initialize: ( String? bio_type) -> void + end + + TruncateExtendedBiosSql: String + + InsertExtraTypesSql: String + + class InsertExtraTypesArgs + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + GetExtraTypesSql: String + + class GetExtraTypesRow + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + TruncateExtraTypesSql: String + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: bool + attr_reader c1: Integer + def initialize: ( bool c0, Integer c1) -> void + end + + class ManyParamsArgs + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: Array[Integer] + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: Array[Integer] + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + def initialize: ( bool c0, Integer c1, String c2, Float c3) -> void + end + + class QuerySql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> CreateAuthorRow? + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (InsertExtraTypesArgs) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/Gemfile b/test/golden/pg/ruby31_gemfile/Gemfile similarity index 100% rename from test/golden/pg_extended/ruby31_gemfile/Gemfile rename to test/golden/pg/ruby31_gemfile/Gemfile diff --git a/test/golden/pg/ruby31_gemfile/models.rb b/test/golden/pg/ruby31_gemfile/models.rb new file mode 100644 index 0000000..32666cb --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/models.rb @@ -0,0 +1,53 @@ +# auto-generated by sqlc - do not edit + +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Struct.new(id, name, bio) + end + + class NodePostgresType < Struct.new( + c_bit, + c_smallint, + c_boolean, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_numeric, + c_real, + c_double_precision, + c_date, + c_time, + c_timestamp, + c_char, + c_varchar, + c_bytea, + c_text, + c_json + ) + end + + class Book < Struct.new(id, name, author_id, description) + end + + class PostgresExtraType < Struct.new( + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ) + end + + class ExtendedBio < Struct.new(author_name, name, bio_type) + end +end \ No newline at end of file diff --git a/test/golden/pg/ruby31_gemfile/models.rbs b/test/golden/pg/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..403270a --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/models.rbs @@ -0,0 +1,93 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class NodePostgresType < ::Struct + attr_accessor c_bit: Array[Integer]? + attr_accessor c_smallint: Integer? + attr_accessor c_boolean: bool? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer? + attr_accessor c_decimal: Float? + attr_accessor c_numeric: Float? + attr_accessor c_real: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: String? + attr_accessor c_time: String? + attr_accessor c_timestamp: String? + attr_accessor c_char: String? + attr_accessor c_varchar: String? + attr_accessor c_bytea: Array[Integer]? + attr_accessor c_text: String? + attr_accessor c_json: String? + def self.new: ( + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json + ) -> NodePostgresType + end + + class Book < ::Struct + attr_accessor id: String + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( String id, String name, Integer author_id, String? description) -> Book + end + + class PostgresExtraType < ::Struct + attr_accessor c_uuid: String? + attr_accessor c_jsonb: untyped + attr_accessor c_text_array: Array[String]? + attr_accessor c_integer_array: Array[Integer]? + attr_accessor c_timestamptz: Time? + attr_accessor c_inet: IPAddr? + attr_accessor c_money: String? + attr_accessor c_not_null: String + def self.new: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> PostgresExtraType + end + + class ExtendedBio < ::Struct + attr_accessor author_name: String + attr_accessor name: String + attr_accessor bio_type: String? + def self.new: ( String author_name, String name, String? bio_type) -> ExtendedBio + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb similarity index 62% rename from test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb rename to test/golden/pg/ruby31_gemfile/my_queries_sql.rb index bc47f95..eebbd37 100644 --- a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class WithNumbers2GoArgs < Struct.new(c0) end @@ -14,12 +15,23 @@ class My_queriesSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] diff --git a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs similarity index 89% rename from test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs rename to test/golden/pg/ruby31_gemfile/my_queries_sql.rbs index 22b7f69..1f667e5 100644 --- a/test/golden/pg_extended/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs @@ -12,6 +12,8 @@ module PgCodegen @prepared_statements: Set[String] def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb similarity index 55% rename from test/golden/pg_extended/ruby31_gemfile/other_sql.rb rename to test/golden/pg/ruby31_gemfile/other_sql.rb index 5e9f4a8..7241699 100644 --- a/test/golden/pg_extended/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class NoColsArgs < Struct.new( c0, @@ -16,22 +17,39 @@ class NoColsArgs < Struct.new( ) end - OtherOneSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class OtherOneRow < Struct.new(c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Struct.new(c0) + end + class OtherSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def no_cols(no_cols_args) @db.with do |client| query_params = [ @@ -62,5 +80,17 @@ def other_one return entity end end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + unless @prepared_statements.include?('count_rows') + client.prepare('count_rows', CountRowsSql) + @prepared_statements.add('count_rows') + end + result = client.exec_prepared('count_rows', query_params) + return result.cmd_tuples + end + end end end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/other_sql.rbs b/test/golden/pg/ruby31_gemfile/other_sql.rbs similarity index 75% rename from test/golden/pg_extended/ruby31_gemfile/other_sql.rbs rename to test/golden/pg/ruby31_gemfile/other_sql.rbs index a4fb600..6a9b51a 100644 --- a/test/golden/pg_extended/ruby31_gemfile/other_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -24,13 +24,24 @@ module PgCodegen def self.new: ( bool c0) -> OtherOneRow end + CountRowsSql: String + + class CountRowsArgs < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> CountRowsArgs + end + class OtherSql @db: untyped @prepared_statements: Set[String] def initialize: (Hash[String, String], Hash[String, String]) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..5d6fca5 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -0,0 +1,671 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' +require_relative 'models' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Struct.new(id, name, bio) + end + + class CreateAuthorArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Struct.new(id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Struct.new( + c_bit, + c_smallint, + c_boolean, + c_integer, + c_bigint, + c_serial, + c_decimal, + c_numeric, + c_real, + c_double_precision, + c_date, + c_time, + c_timestamp, + c_char, + c_varchar, + c_bytea, + c_text, + c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1) + + class ListAuthorsPagedRow < Struct.new(id, name, bio) + end + + class ListAuthorsPagedArgs < Struct.new(offset, limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%')) + + class GetAuthorByNamePatternRow < Struct.new(id, name, bio) + end + + class GetAuthorByNamePatternArgs < Struct.new(name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) + + class CreateAuthorReturnIdRow < Struct.new(id) + end + + class CreateAuthorReturnIdArgs < Struct.new(name, bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Struct.new(bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT [])) + + class GetAuthorsByIdsRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsArgs < Struct.new(param1) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) + + class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsAndNamesArgs < Struct.new(param1, param2) + end + + class CopyAuthorsArgs < Struct.new(name, bio) + end + + TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) + + class CreateBookRow < Struct.new(id) + end + + class CreateBookArgs < Struct.new(name, author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Struct.new(author, book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Struct.new(author, author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1) + + class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) + end + + class GetAuthorsByBookNameArgs < Struct.new(name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) + + class CreateExtendedBioArgs < Struct.new(author_name, name, bio_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Struct.new(author_name, name, bio_type) + end + + class GetFirstExtendedBioByTypeArgs < Struct.new(bio_type) + end + + TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) + + InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) + + class InsertExtraTypesArgs < Struct.new( + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ) + end + + GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) + + class GetExtraTypesRow < Struct.new( + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ) + end + + TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client + client + } + @prepared_statements = Set[] + end + + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + unless @prepared_statements.include?('list_authors_paged') + client.prepare('list_authors_paged', ListAuthorsPagedSql) + @prepared_statements.add('list_authors_paged') + end + result = client.exec_prepared('list_authors_paged', query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + unless @prepared_statements.include?('get_author_by_name_pattern') + client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) + @prepared_statements.add('get_author_by_name_pattern') + end + result = client.exec_prepared('get_author_by_name_pattern', query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + unless @prepared_statements.include?('create_author_return_id') + client.prepare('create_author_return_id', CreateAuthorReturnIdSql) + @prepared_statements.add('create_author_return_id') + end + result = client.exec_prepared('create_author_return_id', query_params) + return result.first['id'] + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + unless @prepared_statements.include?('update_author_bios') + client.prepare('update_author_bios', UpdateAuthorBiosSql) + @prepared_statements.add('update_author_bios') + end + result = client.exec_prepared('update_author_bios', query_params) + return result.cmd_tuples + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [get_authors_by_ids_args.param1] + unless @prepared_statements.include?('get_authors_by_ids') + client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) + @prepared_statements.add('get_authors_by_ids') + end + result = client.exec_prepared('get_authors_by_ids', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] + unless @prepared_statements.include?('get_authors_by_ids_and_names') + client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) + @prepared_statements.add('get_authors_by_ids_and_names') + end + result = client.exec_prepared('get_authors_by_ids_and_names', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + copy_authors_args.each do |item| + client.put_copy_data([item.name, item.bio]) + end + end + end + end + + def truncate_authors + @db.with do |client| + unless @prepared_statements.include?('truncate_authors') + client.prepare('truncate_authors', TruncateAuthorsSql) + @prepared_statements.add('truncate_authors') + end + client.exec_prepared('truncate_authors') + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + unless @prepared_statements.include?('create_book') + client.prepare('create_book', CreateBookSql) + @prepared_statements.add('create_book') + end + result = client.exec_prepared('create_book', query_params) + return result.first['id'] + end + end + + def list_all_authors_books + @db.with do |client| + unless @prepared_statements.include?('list_all_authors_books') + client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) + @prepared_statements.add('list_all_authors_books') + end + result = client.exec_prepared('list_all_authors_books').values + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def get_duplicate_authors + @db.with do |client| + unless @prepared_statements.include?('get_duplicate_authors') + client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) + @prepared_statements.add('get_duplicate_authors') + end + result = client.exec_prepared('get_duplicate_authors').values + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + unless @prepared_statements.include?('get_authors_by_book_name') + client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) + @prepared_statements.add('get_authors_by_book_name') + end + result = client.exec_prepared('get_authors_by_book_name', query_params).values + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] + unless @prepared_statements.include?('create_extended_bio') + client.prepare('create_extended_bio', CreateExtendedBioSql) + @prepared_statements.add('create_extended_bio') + end + client.exec_prepared('create_extended_bio', query_params) + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + unless @prepared_statements.include?('get_first_extended_bio_by_type') + client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) + @prepared_statements.add('get_first_extended_bio_by_type') + end + result = client.exec_prepared('get_first_extended_bio_by_type', query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + return entity + end + end + + def truncate_extended_bios + @db.with do |client| + unless @prepared_statements.include?('truncate_extended_bios') + client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) + @prepared_statements.add('truncate_extended_bios') + end + client.exec_prepared('truncate_extended_bios') + end + end + + def insert_extra_types(insert_extra_types_args) + @db.with do |client| + query_params = [ + insert_extra_types_args.c_uuid, + insert_extra_types_args.c_jsonb, + insert_extra_types_args.c_text_array, + insert_extra_types_args.c_integer_array, + insert_extra_types_args.c_timestamptz, + insert_extra_types_args.c_inet, + insert_extra_types_args.c_money, + insert_extra_types_args.c_not_null + ] + unless @prepared_statements.include?('insert_extra_types') + client.prepare('insert_extra_types', InsertExtraTypesSql) + @prepared_statements.add('insert_extra_types') + end + client.exec_prepared('insert_extra_types', query_params) + end + end + + def get_extra_types + @db.with do |client| + unless @prepared_statements.include?('get_extra_types') + client.prepare('get_extra_types', GetExtraTypesSql) + @prepared_statements.add('get_extra_types') + end + result = client.exec_prepared('get_extra_types') + row = result.first + return nil if row.nil? + entity = GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + return entity + end + end + + def truncate_extra_types + @db.with do |client| + unless @prepared_statements.include?('truncate_extra_types') + client.prepare('truncate_extra_types', TruncateExtraTypesSql) + @prepared_statements.add('truncate_extra_types') + end + client.exec_prepared('truncate_extra_types') + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..9fd1201 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,408 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> CreateAuthorRow + end + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + TestSql: String + + class TestRow < ::Struct + attr_accessor c_bit: Array[Integer]? + attr_accessor c_smallint: Integer? + attr_accessor c_boolean: bool? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer? + attr_accessor c_decimal: Float? + attr_accessor c_numeric: Float? + attr_accessor c_real: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: String? + attr_accessor c_time: String? + attr_accessor c_timestamp: String? + attr_accessor c_char: String? + attr_accessor c_varchar: String? + attr_accessor c_bytea: Array[Integer]? + attr_accessor c_text: String? + attr_accessor c_json: String? + def self.new: ( + Array[Integer]? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + Float? c_decimal, + Float? c_numeric, + Float? c_real, + Float? c_double_precision, + String? c_date, + String? c_time, + String? c_timestamp, + String? c_char, + String? c_varchar, + Array[Integer]? c_bytea, + String? c_text, + String? c_json + ) -> TestRow + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + class ListAuthorsPagedArgs < ::Struct + attr_accessor offset: Integer + attr_accessor limit: Integer + def self.new: ( Integer offset, Integer limit) -> ListAuthorsPagedArgs + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + class GetAuthorByNamePatternArgs < ::Struct + attr_accessor name_pattern: String? + def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateAuthorReturnIdRow + end + + class CreateAuthorReturnIdArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs < ::Struct + attr_accessor bio: String? + def self.new: ( String? bio) -> UpdateAuthorBiosArgs + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + class GetAuthorsByIdsArgs < ::Struct + attr_accessor param1: Array[Integer] + def self.new: ( Array[Integer] param1) -> GetAuthorsByIdsArgs + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class GetAuthorsByIdsAndNamesArgs < ::Struct + attr_accessor param1: Array[Integer] + attr_accessor param2: Array[String] + def self.new: ( Array[Integer] param1, Array[String] param2) -> GetAuthorsByIdsAndNamesArgs + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow < ::Struct + attr_accessor id: String + def self.new: ( String id) -> CreateBookRow + end + + class CreateBookArgs < ::Struct + attr_accessor name: String + attr_accessor author_id: Integer + def self.new: ( String name, Integer author_id) -> CreateBookArgs + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + class GetAuthorsByBookNameArgs < ::Struct + attr_accessor name: String + def self.new: ( String name) -> GetAuthorsByBookNameArgs + end + + CreateExtendedBioSql: String + + class CreateExtendedBioArgs < ::Struct + attr_accessor author_name: String + attr_accessor name: String + attr_accessor bio_type: String? + def self.new: ( String author_name, String name, String? bio_type) -> CreateExtendedBioArgs + end + + GetFirstExtendedBioByTypeSql: String + + class GetFirstExtendedBioByTypeRow < ::Struct + attr_accessor author_name: String + attr_accessor name: String + attr_accessor bio_type: String? + def self.new: ( String author_name, String name, String? bio_type) -> GetFirstExtendedBioByTypeRow + end + + class GetFirstExtendedBioByTypeArgs < ::Struct + attr_accessor bio_type: String? + def self.new: ( String? bio_type) -> GetFirstExtendedBioByTypeArgs + end + + TruncateExtendedBiosSql: String + + InsertExtraTypesSql: String + + class InsertExtraTypesArgs < ::Struct + attr_accessor c_uuid: String? + attr_accessor c_jsonb: untyped + attr_accessor c_text_array: Array[String]? + attr_accessor c_integer_array: Array[Integer]? + attr_accessor c_timestamptz: Time? + attr_accessor c_inet: IPAddr? + attr_accessor c_money: String? + attr_accessor c_not_null: String + def self.new: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> InsertExtraTypesArgs + end + + GetExtraTypesSql: String + + class GetExtraTypesRow < ::Struct + attr_accessor c_uuid: String? + attr_accessor c_jsonb: untyped + attr_accessor c_text_array: Array[String]? + attr_accessor c_integer_array: Array[Integer]? + attr_accessor c_timestamptz: Time? + attr_accessor c_inet: IPAddr? + attr_accessor c_money: String? + attr_accessor c_not_null: String + def self.new: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> GetExtraTypesRow + end + + TruncateExtraTypesSql: String + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + def self.new: ( bool c0, Integer c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: Array[Integer] + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: Array[Integer] + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + Array[Integer] c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + def self.new: ( bool c0, Integer c1, String c2, Float c3) -> FourColsRow + end + + class QuerySql + @db: untyped + @prepared_statements: Set[String] + def initialize: (Hash[String, String], Hash[String, String]) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> CreateAuthorRow? + + def delete_author: (DeleteAuthorArgs) -> void + + def test: -> TestRow? + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (CreateExtendedBioArgs) -> void + + def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (InsertExtraTypesArgs) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + end +end \ No newline at end of file diff --git a/test/golden/pg/ruby32_notypes/models.rb b/test/golden/pg/ruby32_notypes/models.rb new file mode 100644 index 0000000..9539cb7 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/models.rb @@ -0,0 +1,53 @@ +# auto-generated by sqlc - do not edit + +module PgCodegen + module ExtendedBioType + AUTOBIOGRAPHY = "Autobiography" + BIOGRAPHY = "Biography" + MEMOIR = "Memoir" + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb similarity index 62% rename from test/golden/pg_extended/default/my_queries_sql.rb rename to test/golden/pg/ruby32_notypes/my_queries_sql.rb index 46a3001..14da5d7 100644 --- a/test/golden/pg_extended/default/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + WithNumbers2GoSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class WithNumbers2GoArgs < Data.define(:c0) end @@ -14,12 +15,23 @@ class My_queriesSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] diff --git a/test/golden/pg_extended/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb similarity index 56% rename from test/golden/pg_extended/ruby32_notypes/other_sql.rb rename to test/golden/pg/ruby32_notypes/other_sql.rb index 1681c08..33e656e 100644 --- a/test/golden/pg_extended/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -2,10 +2,11 @@ require 'connection_pool' require 'pg' require 'set' +require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + NoColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class NoColsArgs < Data.define( :c0, @@ -16,22 +17,39 @@ class NoColsArgs < Data.define( ) end - OtherOneSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) + OtherOneSql = %q(SELECT 1 -- ? ? + WHERE a = ?) class OtherOneRow < Data.define(:c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + class OtherSql def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } @prepared_statements = Set[] end + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + def no_cols(no_cols_args) @db.with do |client| query_params = [ @@ -62,5 +80,17 @@ def other_one return entity end end + + def count_rows(count_rows_args) + @db.with do |client| + query_params = [count_rows_args.c0] + unless @prepared_statements.include?('count_rows') + client.prepare('count_rows', CountRowsSql) + @prepared_statements.add('count_rows') + end + result = client.exec_prepared('count_rows', query_params) + return result.cmd_tuples + end + end end end \ No newline at end of file diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..7c250a0 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -0,0 +1,671 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require 'set' +require_relative 'models' + +module PgCodegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio) + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = $1) + + class DeleteAuthorArgs < Data.define(:id) + end + + TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1) + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT [])) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:param1) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) + + class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) + end + + GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) + end + + TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) + + InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) + + class InsertExtraTypesArgs < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(connection_pool_params, pg_params) + @db = ConnectionPool.new(**connection_pool_params) { + client = PG.connect(**pg_params) + type_map = PG::BasicTypeMapForResults.new client + type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new client + client + } + @prepared_statements = Set[] + end + + def transaction + @db.with do |client| + client.transaction do + yield self + end + end + end + + def get_author(get_author_args) + @db.with do |client| + query_params = [get_author_args.id] + unless @prepared_statements.include?('get_author') + client.prepare('get_author', GetAuthorSql) + @prepared_statements.add('get_author') + end + result = client.exec_prepared('get_author', query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def list_authors + @db.with do |client| + unless @prepared_statements.include?('list_authors') + client.prepare('list_authors', ListAuthorsSql) + @prepared_statements.add('list_authors') + end + result = client.exec_prepared('list_authors') + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author(create_author_args) + @db.with do |client| + query_params = [create_author_args.name, create_author_args.bio] + unless @prepared_statements.include?('create_author') + client.prepare('create_author', CreateAuthorSql) + @prepared_statements.add('create_author') + end + result = client.exec_prepared('create_author', query_params) + row = result.first + return nil if row.nil? + entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) + return entity + end + end + + def delete_author(delete_author_args) + @db.with do |client| + query_params = [delete_author_args.id] + unless @prepared_statements.include?('delete_author') + client.prepare('delete_author', DeleteAuthorSql) + @prepared_statements.add('delete_author') + end + client.exec_prepared('delete_author', query_params) + end + end + + def test + @db.with do |client| + unless @prepared_statements.include?('test') + client.prepare('test', TestSql) + @prepared_statements.add('test') + end + result = client.exec_prepared('test') + row = result.first + return nil if row.nil? + entity = TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + return entity + end + end + + def list_authors_paged(list_authors_paged_args) + @db.with do |client| + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + unless @prepared_statements.include?('list_authors_paged') + client.prepare('list_authors_paged', ListAuthorsPagedSql) + @prepared_statements.add('list_authors_paged') + end + result = client.exec_prepared('list_authors_paged', query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + @db.with do |client| + query_params = [get_author_by_name_pattern_args.name_pattern] + unless @prepared_statements.include?('get_author_by_name_pattern') + client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) + @prepared_statements.add('get_author_by_name_pattern') + end + result = client.exec_prepared('get_author_by_name_pattern', query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def create_author_return_id(create_author_return_id_args) + @db.with do |client| + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + unless @prepared_statements.include?('create_author_return_id') + client.prepare('create_author_return_id', CreateAuthorReturnIdSql) + @prepared_statements.add('create_author_return_id') + end + result = client.exec_prepared('create_author_return_id', query_params) + return result.first['id'] + end + end + + def update_author_bios(update_author_bios_args) + @db.with do |client| + query_params = [update_author_bios_args.bio] + unless @prepared_statements.include?('update_author_bios') + client.prepare('update_author_bios', UpdateAuthorBiosSql) + @prepared_statements.add('update_author_bios') + end + result = client.exec_prepared('update_author_bios', query_params) + return result.cmd_tuples + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + @db.with do |client| + query_params = [get_authors_by_ids_args.param1] + unless @prepared_statements.include?('get_authors_by_ids') + client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) + @prepared_statements.add('get_authors_by_ids') + end + result = client.exec_prepared('get_authors_by_ids', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + @db.with do |client| + query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] + unless @prepared_statements.include?('get_authors_by_ids_and_names') + client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) + @prepared_statements.add('get_authors_by_ids_and_names') + end + result = client.exec_prepared('get_authors_by_ids_and_names', query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + return entities + end + end + + def copy_authors(copy_authors_args) + @db.with do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + copy_authors_args.each do |item| + client.put_copy_data([item.name, item.bio]) + end + end + end + end + + def truncate_authors + @db.with do |client| + unless @prepared_statements.include?('truncate_authors') + client.prepare('truncate_authors', TruncateAuthorsSql) + @prepared_statements.add('truncate_authors') + end + client.exec_prepared('truncate_authors') + end + end + + def create_book(create_book_args) + @db.with do |client| + query_params = [create_book_args.name, create_book_args.author_id] + unless @prepared_statements.include?('create_book') + client.prepare('create_book', CreateBookSql) + @prepared_statements.add('create_book') + end + result = client.exec_prepared('create_book', query_params) + return result.first['id'] + end + end + + def list_all_authors_books + @db.with do |client| + unless @prepared_statements.include?('list_all_authors_books') + client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) + @prepared_statements.add('list_all_authors_books') + end + result = client.exec_prepared('list_all_authors_books').values + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def get_duplicate_authors + @db.with do |client| + unless @prepared_statements.include?('get_duplicate_authors') + client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) + @prepared_statements.add('get_duplicate_authors') + end + result = client.exec_prepared('get_duplicate_authors').values + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + @db.with do |client| + query_params = [get_authors_by_book_name_args.name] + unless @prepared_statements.include?('get_authors_by_book_name') + client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) + @prepared_statements.add('get_authors_by_book_name') + end + result = client.exec_prepared('get_authors_by_book_name', query_params).values + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + end + end + + def create_extended_bio(create_extended_bio_args) + @db.with do |client| + query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] + unless @prepared_statements.include?('create_extended_bio') + client.prepare('create_extended_bio', CreateExtendedBioSql) + @prepared_statements.add('create_extended_bio') + end + client.exec_prepared('create_extended_bio', query_params) + end + end + + def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) + @db.with do |client| + query_params = [get_first_extended_bio_by_type_args.bio_type] + unless @prepared_statements.include?('get_first_extended_bio_by_type') + client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) + @prepared_statements.add('get_first_extended_bio_by_type') + end + result = client.exec_prepared('get_first_extended_bio_by_type', query_params) + row = result.first + return nil if row.nil? + entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + return entity + end + end + + def truncate_extended_bios + @db.with do |client| + unless @prepared_statements.include?('truncate_extended_bios') + client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) + @prepared_statements.add('truncate_extended_bios') + end + client.exec_prepared('truncate_extended_bios') + end + end + + def insert_extra_types(insert_extra_types_args) + @db.with do |client| + query_params = [ + insert_extra_types_args.c_uuid, + insert_extra_types_args.c_jsonb, + insert_extra_types_args.c_text_array, + insert_extra_types_args.c_integer_array, + insert_extra_types_args.c_timestamptz, + insert_extra_types_args.c_inet, + insert_extra_types_args.c_money, + insert_extra_types_args.c_not_null + ] + unless @prepared_statements.include?('insert_extra_types') + client.prepare('insert_extra_types', InsertExtraTypesSql) + @prepared_statements.add('insert_extra_types') + end + client.exec_prepared('insert_extra_types', query_params) + end + end + + def get_extra_types + @db.with do |client| + unless @prepared_statements.include?('get_extra_types') + client.prepare('get_extra_types', GetExtraTypesSql) + @prepared_statements.add('get_extra_types') + end + result = client.exec_prepared('get_extra_types') + row = result.first + return nil if row.nil? + entity = GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + return entity + end + end + + def truncate_extra_types + @db.with do |client| + unless @prepared_statements.include?('truncate_extra_types') + client.prepare('truncate_extra_types', TruncateExtraTypesSql) + @prepared_statements.add('truncate_extra_types') + end + client.exec_prepared('truncate_extra_types') + end + end + + def many_params(many_params_args) + @db.with do |client| + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + unless @prepared_statements.include?('many_params') + client.prepare('many_params', ManyParamsSql) + @prepared_statements.add('many_params') + end + result = client.exec_prepared('many_params', query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row['c0'], row['c1']) + return entity + end + end + + def many_rows(many_rows_args) + @db.with do |client| + query_params = [many_rows_args.c0] + unless @prepared_statements.include?('many_rows') + client.prepare('many_rows', manyRowsSql) + @prepared_statements.add('many_rows') + end + result = client.exec_prepared('many_rows', query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + return entities + end + end + + def four_cols + @db.with do |client| + unless @prepared_statements.include?('four_cols') + client.prepare('four_cols', FourColsSql) + @prepared_statements.add('four_cols') + end + result = client.exec_prepared('four_cols') + entities = [] + result.each do |row| + entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + return entities + end + end + end +end \ No newline at end of file diff --git a/test/golden/pg_extended/default/options.json b/test/golden/pg_extended/default/options.json deleted file mode 100644 index 2c63c08..0000000 --- a/test/golden/pg_extended/default/options.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/test/golden/pg_extended/default/query_sql.rb b/test/golden/pg_extended/default/query_sql.rb deleted file mode 100644 index f9e0f67..0000000 --- a/test/golden/pg_extended/default/query_sql.rb +++ /dev/null @@ -1,259 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'pg' -require 'set' - -module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Data.define(:id, :name, :bio) - end - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client - client - } - @prepared_statements = Set[] - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') - client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - unless @prepared_statements.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - unless @prepared_statements.include?('test') - client.prepare('test', TestSql) - @prepared_statements.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - unless @prepared_statements.include?('many_params') - client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - unless @prepared_statements.include?('four_cols') - client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/pg_extended/default/query_sql.rbs b/test/golden/pg_extended/default/query_sql.rbs deleted file mode 100644 index c0ae90a..0000000 --- a/test/golden/pg_extended/default/query_sql.rbs +++ /dev/null @@ -1,167 +0,0 @@ -# auto-generated by sqlc - do not edit -module PgCodegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: Array[Integer] - attr_reader c_smallint: Integer - attr_reader c_boolean: bool - attr_reader c_integer: Integer - attr_reader c_bigint: Integer - attr_reader c_serial: Integer - attr_reader c_decimal: Float - attr_reader c_numeric: Float - attr_reader c_real: Float - attr_reader c_double_precision: Float - attr_reader c_date: String - attr_reader c_time: String - attr_reader c_timestamp: String - attr_reader c_char: String - attr_reader c_varchar: String - attr_reader c_bytea: Array[Integer] - attr_reader c_text: String - attr_reader c_json: String - def initialize: ( - Array[Integer] c_bit, - Integer c_smallint, - bool c_boolean, - Integer c_integer, - Integer c_bigint, - Integer c_serial, - Float c_decimal, - Float c_numeric, - Float c_real, - Float c_double_precision, - String c_date, - String c_time, - String c_timestamp, - String c_char, - String c_varchar, - Array[Integer] c_bytea, - String c_text, - String c_json - ) -> void - end - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: bool - attr_reader c1: Integer - def initialize: ( bool c0, Integer c1) -> void - end - - class ManyParamsArgs - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - attr_reader c4: Array[Integer] - def initialize: ( - bool c0, - Integer c1, - String c2, - Float c3, - Array[Integer] c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - attr_reader c4: Array[Integer] - def initialize: ( - bool c0, - Integer c1, - String c2, - Float c3, - Array[Integer] c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: bool - def initialize: ( bool c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - def initialize: ( bool c0, Integer c1, String c2, Float c3) -> void - end - - class QuerySql - @db: untyped - @prepared_statements: Set[String] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> CreateAuthorRow? - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/options.json b/test/golden/pg_extended/ruby31_gemfile/options.json deleted file mode 100644 index 8e3677f..0000000 --- a/test/golden/pg_extended/ruby31_gemfile/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.1", - "generateGemfile": true -} diff --git a/test/golden/pg_extended/ruby31_gemfile/query_sql.rb b/test/golden/pg_extended/ruby31_gemfile/query_sql.rb deleted file mode 100644 index fbcb595..0000000 --- a/test/golden/pg_extended/ruby31_gemfile/query_sql.rb +++ /dev/null @@ -1,259 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'pg' -require 'set' - -module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Struct.new(id, name, bio) - end - - class CreateAuthorArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Struct.new(id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Struct.new( - c_bit, - c_smallint, - c_boolean, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_numeric, - c_real, - c_double_precision, - c_date, - c_time, - c_timestamp, - c_char, - c_varchar, - c_bytea, - c_text, - c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client - client - } - @prepared_statements = Set[] - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') - client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - unless @prepared_statements.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - unless @prepared_statements.include?('test') - client.prepare('test', TestSql) - @prepared_statements.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - unless @prepared_statements.include?('many_params') - client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - unless @prepared_statements.include?('four_cols') - client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs b/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs deleted file mode 100644 index 65fb25a..0000000 --- a/test/golden/pg_extended/ruby31_gemfile/query_sql.rbs +++ /dev/null @@ -1,167 +0,0 @@ -# auto-generated by sqlc - do not edit -module PgCodegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> CreateAuthorRow - end - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String - def self.new: ( String name, String bio) -> CreateAuthorArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - TestSql: String - - class TestRow < ::Struct - attr_accessor c_bit: Array[Integer] - attr_accessor c_smallint: Integer - attr_accessor c_boolean: bool - attr_accessor c_integer: Integer - attr_accessor c_bigint: Integer - attr_accessor c_serial: Integer - attr_accessor c_decimal: Float - attr_accessor c_numeric: Float - attr_accessor c_real: Float - attr_accessor c_double_precision: Float - attr_accessor c_date: String - attr_accessor c_time: String - attr_accessor c_timestamp: String - attr_accessor c_char: String - attr_accessor c_varchar: String - attr_accessor c_bytea: Array[Integer] - attr_accessor c_text: String - attr_accessor c_json: String - def self.new: ( - Array[Integer] c_bit, - Integer c_smallint, - bool c_boolean, - Integer c_integer, - Integer c_bigint, - Integer c_serial, - Float c_decimal, - Float c_numeric, - Float c_real, - Float c_double_precision, - String c_date, - String c_time, - String c_timestamp, - String c_char, - String c_varchar, - Array[Integer] c_bytea, - String c_text, - String c_json - ) -> TestRow - end - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - def self.new: ( bool c0, Integer c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - attr_accessor c4: Array[Integer] - def self.new: ( - bool c0, - Integer c1, - String c2, - Float c3, - Array[Integer] c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - attr_accessor c4: Array[Integer] - def self.new: ( - bool c0, - Integer c1, - String c2, - Float c3, - Array[Integer] c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: bool - def self.new: ( bool c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - def self.new: ( bool c0, Integer c1, String c2, Float c3) -> FourColsRow - end - - class QuerySql - @db: untyped - @prepared_statements: Set[String] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> CreateAuthorRow? - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/pg_extended/ruby32_notypes/options.json b/test/golden/pg_extended/ruby32_notypes/options.json deleted file mode 100644 index ea1ba9b..0000000 --- a/test/golden/pg_extended/ruby32_notypes/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.2", - "generateTypes": false -} diff --git a/test/golden/pg_extended/ruby32_notypes/query_sql.rb b/test/golden/pg_extended/ruby32_notypes/query_sql.rb deleted file mode 100644 index f9e0f67..0000000 --- a/test/golden/pg_extended/ruby32_notypes/query_sql.rb +++ /dev/null @@ -1,259 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'pg' -require 'set' - -module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Data.define(:id, :name, :bio) - end - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - ManyParamsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- $1 $2 - WHERE a = $3) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - client.type_map_for_results = PG::BasicTypeMapForResults.new client - client - } - @prepared_statements = Set[] - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') - client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - unless @prepared_statements.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - unless @prepared_statements.include?('test') - client.prepare('test', TestSql) - @prepared_statements.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - unless @prepared_statements.include?('many_params') - client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - unless @prepared_statements.include?('four_cols') - client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file diff --git a/test/golden/sqlite3/default/models.rb b/test/golden/sqlite3/default/models.rb new file mode 100644 index 0000000..c96b91e --- /dev/null +++ b/test/golden/sqlite3/default/models.rb @@ -0,0 +1,9 @@ +# auto-generated by sqlc - do not edit + +module Sqlite3Codegen + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end \ No newline at end of file diff --git a/test/golden/sqlite3/default/models.rbs b/test/golden/sqlite3/default/models.rbs new file mode 100644 index 0000000..df9c5ba --- /dev/null +++ b/test/golden/sqlite3/default/models.rbs @@ -0,0 +1,17 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/my_queries_sql.rb b/test/golden/sqlite3/default/my_queries_sql.rb similarity index 72% rename from test/golden/sqlite3_extended/default/my_queries_sql.rb rename to test/golden/sqlite3/default/my_queries_sql.rb index 5fb9256..b6a7fc5 100644 --- a/test/golden/sqlite3_extended/default/my_queries_sql.rb +++ b/test/golden/sqlite3/default/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -14,11 +15,21 @@ def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def with_numbers2_go(with_numbers2_go_args) client = @db query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/my_queries_sql.rbs b/test/golden/sqlite3/default/my_queries_sql.rbs similarity index 86% rename from test/golden/sqlite3_extended/default/my_queries_sql.rbs rename to test/golden/sqlite3/default/my_queries_sql.rbs index 05db55c..2c1d2d2 100644 --- a/test/golden/sqlite3_extended/default/my_queries_sql.rbs +++ b/test/golden/sqlite3/default/my_queries_sql.rbs @@ -11,6 +11,8 @@ module Sqlite3Codegen @db: untyped def initialize: (untyped) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb b/test/golden/sqlite3/default/other_sql.rb similarity index 50% rename from test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb rename to test/golden/sqlite3/default/other_sql.rb index dcf3ef3..bbdfe6f 100644 --- a/test/golden/sqlite3_extended/ruby32_notypes/other_sql.rb +++ b/test/golden/sqlite3/default/other_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen NoColsSql = %q(SELECT 1 -- ? ? @@ -21,11 +22,23 @@ class NoColsArgs < Data.define( class OtherOneRow < Data.define(:c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + class OtherSql def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def no_cols(no_cols_args) client = @db query_params = [ @@ -36,17 +49,37 @@ def no_cols(no_cols_args) no_cols_args.c4 ] stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end def other_one client = @db stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + ensure + stmt.close + end + end + + def count_rows(count_rows_args) + client = @db + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/other_sql.rbs b/test/golden/sqlite3/default/other_sql.rbs similarity index 74% rename from test/golden/sqlite3_extended/default/other_sql.rbs rename to test/golden/sqlite3/default/other_sql.rbs index c3f60c5..06c3bbd 100644 --- a/test/golden/sqlite3_extended/default/other_sql.rbs +++ b/test/golden/sqlite3/default/other_sql.rbs @@ -24,12 +24,23 @@ module Sqlite3Codegen def initialize: ( Integer c0) -> void end + CountRowsSql: String + + class CountRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + class OtherSql @db: untyped def initialize: (untyped) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/sqlite3/default/query_sql.rb b/test/golden/sqlite3/default/query_sql.rb new file mode 100644 index 0000000..3d09e88 --- /dev/null +++ b/test/golden/sqlite3/default/query_sql.rb @@ -0,0 +1,462 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def transaction + @db.transaction do + yield self + end + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def list_authors_paged(list_authors_paged_args) + client = @db + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + client = @db + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author_return_id(create_author_return_id_args) + client = @db + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def update_author_bios(update_author_bios_args) + client = @db + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + client = @db + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + client = @db + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def copy_authors(copy_authors_args) + client = @db + copy_authors_args.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) + end + end + + def truncate_authors + client = @db + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + + def create_book(create_book_args) + client = @db + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + client = @db + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + ensure + stmt.close + end + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs new file mode 100644 index 0000000..d5a2758 --- /dev/null +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -0,0 +1,273 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + GetAuthorSql: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsSql: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CreateAuthorSql: String + + class CreateAuthorArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class ListAuthorsPagedArgs + attr_reader offset: Integer + attr_reader limit: Integer + def initialize: ( Integer offset, Integer limit) -> void + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorByNamePatternArgs + attr_reader name_pattern: String? + def initialize: ( String? name_pattern) -> void + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateAuthorReturnIdArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs + attr_reader bio: String? + def initialize: ( String? bio) -> void + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsArgs + attr_reader ids: Array[Integer] + def initialize: ( Array[Integer] ids) -> void + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class GetAuthorsByIdsAndNamesArgs + attr_reader ids: Array[Integer] + attr_reader names: Array[String] + def initialize: ( Array[Integer] ids, Array[String] names) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + class CreateBookArgs + attr_reader name: String + attr_reader author_id: Integer + def initialize: ( String name, Integer author_id) -> void + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class GetAuthorsByBookNameArgs + attr_reader name: String + def initialize: ( String name) -> void + end + + ManyParamsSql: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + class ManyParamsArgs + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: bool + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> void + end + + manyRowsSql: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + attr_reader c4: bool + def initialize: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> void + end + + class ManyRowsArgs + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + FourColsSql: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: Array[Integer] + def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + end + + class QuerySql + @db: untyped + def initialize: (untyped) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> void + + def delete_author: (DeleteAuthorArgs) -> void + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + def slice_placeholders: (Array[untyped]) -> String + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/Gemfile b/test/golden/sqlite3/ruby31_gemfile/Gemfile similarity index 100% rename from test/golden/sqlite3_extended/ruby31_gemfile/Gemfile rename to test/golden/sqlite3/ruby31_gemfile/Gemfile diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rb b/test/golden/sqlite3/ruby31_gemfile/models.rb new file mode 100644 index 0000000..7d247be --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/models.rb @@ -0,0 +1,9 @@ +# auto-generated by sqlc - do not edit + +module Sqlite3Codegen + class Author < Struct.new(id, name, bio) + end + + class Book < Struct.new(id, name, author_id, description) + end +end \ No newline at end of file diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rbs b/test/golden/sqlite3/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..3c6a328 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/models.rbs @@ -0,0 +1,17 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class Book < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb similarity index 72% rename from test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb rename to test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb index 9a8e0e7..a4796b1 100644 --- a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -14,11 +15,21 @@ def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def with_numbers2_go(with_numbers2_go_args) client = @db query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs similarity index 87% rename from test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs rename to test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs index fed39ec..1637bf8 100644 --- a/test/golden/sqlite3_extended/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs @@ -11,6 +11,8 @@ module Sqlite3Codegen @db: untyped def initialize: (untyped) -> void + def transaction: [T] () { (self) -> T } -> T + def with_numbers2_go: (WithNumbers2GoArgs) -> void end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb similarity index 50% rename from test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb rename to test/golden/sqlite3/ruby31_gemfile/other_sql.rb index 23a4a2f..af3912a 100644 --- a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen NoColsSql = %q(SELECT 1 -- ? ? @@ -21,11 +22,23 @@ class NoColsArgs < Struct.new( class OtherOneRow < Struct.new(c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Struct.new(c0) + end + class OtherSql def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def no_cols(no_cols_args) client = @db query_params = [ @@ -36,17 +49,37 @@ def no_cols(no_cols_args) no_cols_args.c4 ] stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end def other_one client = @db stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + ensure + stmt.close + end + end + + def count_rows(count_rows_args) + client = @db + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs similarity index 73% rename from test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs rename to test/golden/sqlite3/ruby31_gemfile/other_sql.rbs index 6b68028..64ee08f 100644 --- a/test/golden/sqlite3_extended/ruby31_gemfile/other_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs @@ -24,12 +24,23 @@ module Sqlite3Codegen def self.new: ( Integer c0) -> OtherOneRow end + CountRowsSql: String + + class CountRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> CountRowsArgs + end + class OtherSql @db: untyped def initialize: (untyped) -> void + def transaction: [T] () { (self) -> T } -> T + def no_cols: (NoColsArgs) -> void def other_one: -> OtherOneRow? + + def count_rows: (CountRowsArgs) -> Integer end end \ No newline at end of file diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..7b21662 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -0,0 +1,462 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Struct.new(id, name, bio) + end + + class GetAuthorArgs < Struct.new(id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Struct.new(id, name, bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Struct.new(name, bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Struct.new(id) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1) + + class ListAuthorsPagedRow < Struct.new(id, name, bio) + end + + class ListAuthorsPagedArgs < Struct.new(offset, limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%')) + + class GetAuthorByNamePatternRow < Struct.new(id, name, bio) + end + + class GetAuthorByNamePatternArgs < Struct.new(name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) + + class CreateAuthorReturnIdRow < Struct.new(id) + end + + class CreateAuthorReturnIdArgs < Struct.new(name, bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Struct.new(bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsArgs < Struct.new(ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) + end + + class GetAuthorsByIdsAndNamesArgs < Struct.new(ids, names) + end + + class CopyAuthorsArgs < Struct.new(name, bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) + + class CreateBookRow < Struct.new(id) + end + + class CreateBookArgs < Struct.new(name, author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Struct.new(author, book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Struct.new(author, author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) + end + + class GetAuthorsByBookNameArgs < Struct.new(name) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Struct.new(c0, c1) + end + + class ManyParamsArgs < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Struct.new( + c0, + c1, + c2, + c3, + c4 + ) + end + + class ManyRowsArgs < Struct.new(c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Struct.new(c0, c1, c2, c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def transaction + @db.transaction do + yield self + end + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def list_authors_paged(list_authors_paged_args) + client = @db + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + client = @db + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author_return_id(create_author_return_id_args) + client = @db + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def update_author_bios(update_author_bios_args) + client = @db + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + client = @db + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + client = @db + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def copy_authors(copy_authors_args) + client = @db + copy_authors_args.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) + end + end + + def truncate_authors + client = @db + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + + def create_book(create_book_args) + client = @db + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + client = @db + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + ensure + stmt.close + end + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs new file mode 100644 index 0000000..d6d7efc --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,273 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + GetAuthorSql: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + class GetAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> GetAuthorArgs + end + + ListAuthorsSql: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CreateAuthorSql: String + + class CreateAuthorArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorArgs + end + + DeleteAuthorSql: String + + class DeleteAuthorArgs < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> DeleteAuthorArgs + end + + ListAuthorsPagedSql: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + class ListAuthorsPagedArgs < ::Struct + attr_accessor offset: Integer + attr_accessor limit: Integer + def self.new: ( Integer offset, Integer limit) -> ListAuthorsPagedArgs + end + + GetAuthorByNamePatternSql: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + class GetAuthorByNamePatternArgs < ::Struct + attr_accessor name_pattern: String? + def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs + end + + CreateAuthorReturnIdSql: String + + class CreateAuthorReturnIdRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateAuthorReturnIdRow + end + + class CreateAuthorReturnIdArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs + end + + UpdateAuthorBiosSql: String + + class UpdateAuthorBiosArgs < ::Struct + attr_accessor bio: String? + def self.new: ( String? bio) -> UpdateAuthorBiosArgs + end + + GetAuthorsByIdsSql: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + class GetAuthorsByIdsArgs < ::Struct + attr_accessor ids: Array[Integer] + def self.new: ( Array[Integer] ids) -> GetAuthorsByIdsArgs + end + + GetAuthorsByIdsAndNamesSql: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class GetAuthorsByIdsAndNamesArgs < ::Struct + attr_accessor ids: Array[Integer] + attr_accessor names: Array[String] + def self.new: ( Array[Integer] ids, Array[String] names) -> GetAuthorsByIdsAndNamesArgs + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TruncateAuthorsSql: String + + CreateBookSql: String + + class CreateBookRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateBookRow + end + + class CreateBookArgs < ::Struct + attr_accessor name: String + attr_accessor author_id: Integer + def self.new: ( String name, Integer author_id) -> CreateBookArgs + end + + ListAllAuthorsBooksSql: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GetDuplicateAuthorsSql: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GetAuthorsByBookNameSql: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + class GetAuthorsByBookNameArgs < ::Struct + attr_accessor name: String + def self.new: ( String name) -> GetAuthorsByBookNameArgs + end + + ManyParamsSql: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + class ManyParamsArgs < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: bool + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> ManyParamsArgs + end + + manyRowsSql: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + attr_accessor c4: bool + def self.new: ( + Integer c0, + String c1, + Float c2, + Array[Integer] c3, + bool c4 + ) -> ManyRowsRow + end + + class ManyRowsArgs < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> ManyRowsArgs + end + + FourColsSql: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: Array[Integer] + def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + end + + class QuerySql + @db: untyped + def initialize: (untyped) -> void + + def transaction: [T] () { (self) -> T } -> T + + def get_author: (GetAuthorArgs) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (CreateAuthorArgs) -> void + + def delete_author: (DeleteAuthorArgs) -> void + + def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer + + def update_author_bios: (UpdateAuthorBiosArgs) -> Integer + + def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs]) -> void + + def truncate_authors: -> void + + def create_book: (CreateBookArgs) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] + + def many_params: (ManyParamsArgs) -> ManyParamsRow? + + def many_rows: (manyRowsArgs) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + def slice_placeholders: (Array[untyped]) -> String + end +end \ No newline at end of file diff --git a/test/golden/sqlite3/ruby32_notypes/models.rb b/test/golden/sqlite3/ruby32_notypes/models.rb new file mode 100644 index 0000000..c96b91e --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/models.rb @@ -0,0 +1,9 @@ +# auto-generated by sqlc - do not edit + +module Sqlite3Codegen + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb similarity index 72% rename from test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb rename to test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb index 5fb9256..b6a7fc5 100644 --- a/test/golden/sqlite3_extended/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen WithNumbers2GoSql = %q(SELECT 1 -- ? ? @@ -14,11 +15,21 @@ def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def with_numbers2_go(with_numbers2_go_args) client = @db query_params = [with_numbers2_go_args.c0] stmt = client.prepare(WithNumbers2GoSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/other_sql.rb b/test/golden/sqlite3/ruby32_notypes/other_sql.rb similarity index 50% rename from test/golden/sqlite3_extended/default/other_sql.rb rename to test/golden/sqlite3/ruby32_notypes/other_sql.rb index dcf3ef3..bbdfe6f 100644 --- a/test/golden/sqlite3_extended/default/other_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/other_sql.rb @@ -1,6 +1,7 @@ # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' +require_relative 'models' module Sqlite3Codegen NoColsSql = %q(SELECT 1 -- ? ? @@ -21,11 +22,23 @@ class NoColsArgs < Data.define( class OtherOneRow < Data.define(:c0) end + CountRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class CountRowsArgs < Data.define(:c0) + end + class OtherSql def initialize(db) @db = db end + def transaction + @db.transaction do + yield self + end + end + def no_cols(no_cols_args) client = @db query_params = [ @@ -36,17 +49,37 @@ def no_cols(no_cols_args) no_cols_args.c4 ] stmt = client.prepare(NoColsSql) - stmt.execute(*query_params) + begin + stmt.execute(*query_params) + ensure + stmt.close + end end def other_one client = @db stmt = client.prepare(OtherOneSql) - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity + begin + result = stmt.execute + row = result.first + return nil if row.nil? + entity = OtherOneRow.new(row[0]) + return entity + ensure + stmt.close + end + end + + def count_rows(count_rows_args) + client = @db + query_params = [count_rows_args.c0] + stmt = client.prepare(CountRowsSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end end end end \ No newline at end of file diff --git a/test/golden/sqlite3/ruby32_notypes/query_sql.rb b/test/golden/sqlite3/ruby32_notypes/query_sql.rb new file mode 100644 index 0000000..3d09e88 --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -0,0 +1,462 @@ +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + GetAuthorSql = %q(SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1) + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + class GetAuthorArgs < Data.define(:id) + end + + ListAuthorsSql = %q(SELECT id, name, bio FROM authors + ORDER BY name) + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CreateAuthorSql = %q(INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + )) + + class CreateAuthorArgs < Data.define(:name, :bio) + end + + DeleteAuthorSql = %q(DELETE FROM authors + WHERE id = ?) + + class DeleteAuthorArgs < Data.define(:id) + end + + ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1) + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + class ListAuthorsPagedArgs < Data.define(:offset, :limit) + end + + GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%')) + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + class GetAuthorByNamePatternArgs < Data.define(:name_pattern) + end + + CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + class CreateAuthorReturnIdArgs < Data.define(:name, :bio) + end + + UpdateAuthorBiosSql = %q(UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL) + + class UpdateAuthorBiosArgs < Data.define(:bio) + end + + GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsArgs < Data.define(:ids) + end + + GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TruncateAuthorsSql = %q(DELETE FROM authors) + + CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) + + class CreateBookRow < Data.define(:id) + end + + class CreateBookArgs < Data.define(:name, :author_id) + end + + ListAllAuthorsBooksSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name) + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GetDuplicateAuthorsSql = %q(SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id) + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GetAuthorsByBookNameSql = %q(SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ?) + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class GetAuthorsByBookNameArgs < Data.define(:name) + end + + ManyParamsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyParamsRow < Data.define(:c0, :c1) + end + + class ManyParamsArgs < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + ManyRowsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + class ManyRowsArgs < Data.define(:c0) + end + + FourColsSql = %q(SELECT 1 -- ? ? + WHERE a = ?) + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + def initialize(db) + @db = db + end + + def transaction + @db.transaction do + yield self + end + end + + def get_author(get_author_args) + client = @db + query_params = [get_author_args.id] + stmt = client.prepare(GetAuthorSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = GetAuthorRow.new(row[0], row[1], row[2]) + return entity + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(ListAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAuthorsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author(create_author_args) + client = @db + query_params = [create_author_args.name, create_author_args.bio] + stmt = client.prepare(CreateAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def delete_author(delete_author_args) + client = @db + query_params = [delete_author_args.id] + stmt = client.prepare(DeleteAuthorSql) + begin + stmt.execute(*query_params) + ensure + stmt.close + end + end + + def list_authors_paged(list_authors_paged_args) + client = @db + query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] + stmt = client.prepare(ListAuthorsPagedSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_author_by_name_pattern(get_author_by_name_pattern_args) + client = @db + query_params = [get_author_by_name_pattern_args.name_pattern] + stmt = client.prepare(GetAuthorByNamePatternSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def create_author_return_id(create_author_return_id_args) + client = @db + query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] + stmt = client.prepare(CreateAuthorReturnIdSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def update_author_bios(update_author_bios_args) + client = @db + query_params = [update_author_bios_args.bio] + stmt = client.prepare(UpdateAuthorBiosSql) + begin + stmt.execute(*query_params) + return client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(get_authors_by_ids_args) + client = @db + query_params = [*get_authors_by_ids_args.ids] + sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) + client = @db + query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] + sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + return entities + ensure + stmt.close + end + end + + def copy_authors(copy_authors_args) + client = @db + copy_authors_args.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) + end + end + + def truncate_authors + client = @db + stmt = client.prepare(TruncateAuthorsSql) + begin + stmt.execute + ensure + stmt.close + end + end + + def create_book(create_book_args) + client = @db + query_params = [create_book_args.name, create_book_args.author_id] + stmt = client.prepare(CreateBookSql) + begin + result = stmt.execute(*query_params) + row = result.first + return row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(ListAllAuthorsBooksSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GetDuplicateAuthorsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + return entities + ensure + stmt.close + end + end + + def get_authors_by_book_name(get_authors_by_book_name_args) + client = @db + query_params = [get_authors_by_book_name_args.name] + stmt = client.prepare(GetAuthorsByBookNameSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + return entities + ensure + stmt.close + end + end + + def many_params(many_params_args) + client = @db + query_params = [ + many_params_args.c0, + many_params_args.c1, + many_params_args.c2, + many_params_args.c3, + many_params_args.c4 + ] + stmt = client.prepare(ManyParamsSql) + begin + result = stmt.execute(*query_params) + row = result.first + return nil if row.nil? + entity = ManyParamsRow.new(row[0], row[1]) + return entity + ensure + stmt.close + end + end + + def many_rows(many_rows_args) + client = @db + query_params = [many_rows_args.c0] + stmt = client.prepare(manyRowsSql) + begin + result = stmt.execute(*query_params) + entities = [] + result.each do |row| + entities << manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + return entities + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FourColsSql) + begin + result = stmt.execute + entities = [] + result.each do |row| + entities << FourColsRow.new(row[0], row[1], row[2], row[3]) + end + return entities + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/options.json b/test/golden/sqlite3_extended/default/options.json deleted file mode 100644 index 2c63c08..0000000 --- a/test/golden/sqlite3_extended/default/options.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/test/golden/sqlite3_extended/default/query_sql.rb b/test/golden/sqlite3_extended/default/query_sql.rb deleted file mode 100644 index 5061b6d..0000000 --- a/test/golden/sqlite3_extended/default/query_sql.rb +++ /dev/null @@ -1,159 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'sqlite3' - -module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - end - end -end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/default/query_sql.rbs b/test/golden/sqlite3_extended/default/query_sql.rbs deleted file mode 100644 index 27784f0..0000000 --- a/test/golden/sqlite3_extended/default/query_sql.rbs +++ /dev/null @@ -1,114 +0,0 @@ -# auto-generated by sqlc - do not edit -module Sqlite3Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String - def initialize: ( Integer id, String name, String bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String - def initialize: ( String name, String bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: Integer - attr_reader c1: String - def initialize: ( Integer c0, String c1) -> void - end - - class ManyParamsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: bool - def initialize: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - bool c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: bool - def initialize: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - bool c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: Array[Integer] - def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void - end - - class QuerySql - @db: untyped - def initialize: (untyped) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> void - - def delete_author: (DeleteAuthorArgs) -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/options.json b/test/golden/sqlite3_extended/ruby31_gemfile/options.json deleted file mode 100644 index 8e3677f..0000000 --- a/test/golden/sqlite3_extended/ruby31_gemfile/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.1", - "generateGemfile": true -} diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb deleted file mode 100644 index af36cb9..0000000 --- a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rb +++ /dev/null @@ -1,159 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'sqlite3' - -module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Struct.new(id) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - end - end -end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs deleted file mode 100644 index 08b7cda..0000000 --- a/test/golden/sqlite3_extended/ruby31_gemfile/query_sql.rbs +++ /dev/null @@ -1,114 +0,0 @@ -# auto-generated by sqlc - do not edit -module Sqlite3Codegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String - def self.new: ( Integer id, String name, String bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String - def self.new: ( String name, String bio) -> CreateAuthorArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - def self.new: ( Integer c0, String c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: bool - def self.new: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - bool c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: bool - def self.new: ( - Integer c0, - String c1, - Float c2, - Array[Integer] c3, - bool c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: Array[Integer] - def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow - end - - class QuerySql - @db: untyped - def initialize: (untyped) -> void - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> void - - def create_author: (CreateAuthorArgs) -> void - - def delete_author: (DeleteAuthorArgs) -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> void - - def four_cols: -> void - end -end \ No newline at end of file diff --git a/test/golden/sqlite3_extended/ruby32_notypes/options.json b/test/golden/sqlite3_extended/ruby32_notypes/options.json deleted file mode 100644 index ea1ba9b..0000000 --- a/test/golden/sqlite3_extended/ruby32_notypes/options.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rubyVersion": "3.2", - "generateTypes": false -} diff --git a/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb b/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb deleted file mode 100644 index 5061b6d..0000000 --- a/test/golden/sqlite3_extended/ruby32_notypes/query_sql.rb +++ /dev/null @@ -1,159 +0,0 @@ -# auto-generated by sqlc - do not edit -require 'connection_pool' -require 'sqlite3' - -module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - stmt.execute(*query_params) - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - stmt.execute(*query_params) - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - end - end -end \ No newline at end of file diff --git a/test/golden_test.rb b/test/golden_test.rb index 81213ca..d483417 100644 --- a/test/golden_test.rb +++ b/test/golden_test.rb @@ -2,20 +2,19 @@ require_relative 'test_helper' -# Diffs the generated files against the C# plugin's output checked in under examples/. +# examples/ is the output of the plugin for the example schemas in examples/authors (real sqlc requests +# captured in test/fixtures). This test fails when the checked-in examples are stale: regenerate them with +# `sqlc generate` (CI runs `sqlc diff` for the same purpose). class GoldenTest < Minitest::Test include PluginHelper - { - 'pg' => 'pg', - 'mysql2' => 'mysql2', - 'sqlite3' => 'sqlite3' - }.each do |driver, example| - %w[query_sql.rb query_sql.rbs].each do |name| - define_method("test_#{driver}_#{name.tr('.', '_')}_matches_csharp_output") do - files = generate(load_request(driver)) + { 'pg' => 'pg', 'mysql2' => 'mysql2', 'sqlite3' => 'sqlite3' }.each do |driver, example| + define_method("test_#{driver}_matches_examples") do + files = generate(load_request(driver)) + assert_equal %w[models.rb models.rbs query_sql.rb query_sql.rbs], files.keys.sort + files.each do |name, contents| expected = File.read(File.join(ROOT, 'examples', example, name)) - assert_equal normalize(expected), normalize(files.fetch(name)) + assert_equal normalize(expected), normalize(contents), "examples/#{example}/#{name} is stale" end end end diff --git a/test/synthetic_requests.rb b/test/synthetic_requests.rb new file mode 100644 index 0000000..ac8af9e --- /dev/null +++ b/test/synthetic_requests.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +# Extra queries added on top of a captured sqlc request to hit generator paths the example schemas do not: +# 5+ params / columns (multi-line formatting), lower-case query names, several query files, :execrows, ... +module SyntheticRequests + P = SqlcGenRuby::Plugin + TYPES = { + 'pg' => %w[bool int4 text float8 bytea], + 'mysql2' => %w[int varchar double blob tinyint], + 'sqlite3' => %w[integer text real blob boolean] + }.freeze + + module_function + + def column(name, type, not_null: true) + P::Column.new(name: name, type: P::Identifier.new(name: type), not_null: not_null) + end + + def query(name, cmd, columns, params, file = 'query.sql') + P::Query.new(name: name, cmd: cmd, text: "SELECT 1 -- ? ?\n\t WHERE a = ?", columns: columns, filename: file, + params: params.each_with_index.map { |c, i| P::Parameter.new(number: i + 1, column: c) }) + end + + def queries(driver) + cols = TYPES.fetch(driver).each_with_index.map { |type, i| column("c#{i}", type) } + [ + query('ManyParams', ':one', cols[0, 2], cols), # 5 params -> multi-line formatting + query('manyRows', ':many', cols, cols[0, 1]), # 5 columns, lower-case name + query('FourCols', ':many', cols[0, 4], []), + query('NoCols', ':exec', [], cols, 'other.sql'), + query('OtherOne', ':one', cols[0, 1], [], 'other.sql'), + query('CountRows', ':execrows', [], cols[0, 1], 'other.sql'), + query('WithNumbers2Go', ':exec', [], cols[0, 1], 'my_queries.sql') + ] + end + + def extended_request(driver, **options) + base = PluginHelper.instance_method(:load_request).bind_call(Object.new.extend(PluginHelper), driver, **options) + base.queries.concat(queries(driver)) + base + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index bac54e9..6ae86b1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,6 +2,7 @@ require 'open3' require 'json' +require 'fileutils' require 'minitest/autorun' $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require 'sqlc_gen_ruby/plugin_pb' diff --git a/tests/consts.rb b/tests/consts.rb index 5cfbf24..99ed85d 100644 --- a/tests/consts.rb +++ b/tests/consts.rb @@ -5,4 +5,4 @@ module Consts DR_SEUSS_AUTHOR = "Dr. Seuss" DR_SEUSS_QUOTE = "You'll miss the best things if you keep your eyes shut" DR_SEUSS_NEW_QUOTE = "Today you are You, that is truer than true. There is no one alive who is Youer than You" -end \ No newline at end of file +end diff --git a/tests/end2end_mysql2.rb b/tests/end2end_mysql2.rb index 9860ad5..305bd45 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -4,66 +4,219 @@ require_relative 'consts' require_relative '../examples/mysql2/query_sql' +# End-to-end tests of the code generated for examples/authors/mysql, against a real MySQL / MariaDB server. class TestMysql2 < Minitest::Test - def setup - mysql2_params = { - 'dbname' => ENV['DB_NAME'], - 'host' => ENV['MYSQL_HOST'], - 'username' => ENV['DB_USER'] - } - @query_sql = Mysql2Codegen::QuerySql.new({ }, mysql2_params) - end + include Mysql2Codegen - def test_flow - first_inserted_id = create_first_author_and_assert - create_second_author_and_assert - delete_author_and_assert(first_inserted_id) - end - - def create_first_author_and_assert - create_author_return_id_args = Mysql2Codegen::CreateAuthorReturnIdArgs.new( - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - inserted_id = @query_sql.create_author_return_id(create_author_return_id_args) - get_author_args = Mysql2Codegen::GetAuthorArgs.new(id: inserted_id) - actual = @query_sql.get_author(get_author_args) - expected = Mysql2Codegen::GetAuthorRow.new( - id: inserted_id, - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - assert_equal(expected, actual) - inserted_id + def mysql2_params + { + 'database' => ENV['DB_NAME'], + 'host' => ENV['MYSQL_HOST'], + 'port' => ENV['MYSQL_PORT']&.to_i, # optional + 'username' => ENV['DB_USER'], + 'local_infile' => true # required by :copyfrom (LOAD DATA LOCAL INFILE) + }.compact + end + + def setup + @query_sql = QuerySql.new({}, mysql2_params) + @query_sql.truncate_authors # cascades to books + @query_sql.truncate_extended_bios + end + + def create_author(name, bio = nil) + @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) + @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + + assert_equal GetAuthorRow.new(first, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), + @query_sql.get_author(GetAuthorArgs.new(id: first)) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + assert_equal [Consts::BOJACK_AUTHOR, Consts::DR_SEUSS_AUTHOR].sort, @query_sql.list_authors.map(&:name).sort + + @query_sql.update_author(UpdateAuthorArgs.new(bio: Consts::DR_SEUSS_NEW_QUOTE, id: first)) + assert_equal Consts::DR_SEUSS_NEW_QUOTE, @query_sql.get_author(GetAuthorArgs.new(id: first)).bio + + @query_sql.delete_author(DeleteAuthorArgs.new(id: first)) + assert_equal [Consts::DR_SEUSS_AUTHOR], @query_sql.list_authors.map(&:name) + end + + def test_nullable_columns_round_trip_nil + id = create_author('no bio', nil) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + end + + def test_one_on_empty_table_returns_nil + assert_nil @query_sql.test + end + + def test_prepared_statements_are_closed + id = create_author('a') + 300.times { @query_sql.get_author(GetAuthorArgs.new(id: id)) } + status = Mysql2::Client.new(mysql2_params).query("SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count'").first + assert_equal '0', status['Value'].to_s, 'prepared statements leak on the server' + end + + # ---- :execlastid / :execrows ----------------------------------------------------------------------------------- + + def test_execlastid_returns_the_new_id + first = create_author('a') + second = create_author('b') + assert_kind_of Integer, first + assert_equal first + 1, second + assert_kind_of Integer, @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: first)) + end + + def test_execrows_returns_affected_row_count + create_author('with bio', 'bio') + create_author('with bio 2', 'bio') + create_author('without bio', nil) + assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) + end + + # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- + + def test_positional_args_limit_and_offset + %w[a b c d].each { |name| create_author(name) } + page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + assert_equal %w[b c], page.map(&:name) + end + + def test_nullable_named_arg + %w[abc abd xyz].each { |name| create_author(name) } + assert_equal 3, @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)).size + ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + assert_equal %w[abc abd], ab.map(&:name).sort + end + + # ---- sqlc.slice ------------------------------------------------------------------------------------------------ + + def test_slice + ids = %w[a b c].map { |name| create_author(name) } + got = @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: ids.first(2))) + assert_equal ids.first(2).sort, got.map(&:id).sort + assert_equal [ids.last], @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [ids.last])).map(&:id) + end + + def test_empty_slice_matches_nothing + create_author('a') + assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [])) + end + + def test_several_slices_in_one_query + ids = %w[a b c].map { |name| create_author(name) } + got = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: %w[b c x])) + assert_equal %w[b c], got.map(&:name).sort + assert_empty @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: [])) + end + + # ---- :copyfrom ------------------------------------------------------------------------------------------------- + + def test_copyfrom + rows = [ + CopyAuthorsArgs.new(name: 'plain', bio: 'bio'), + CopyAuthorsArgs.new(name: 'no bio', bio: nil), + CopyAuthorsArgs.new(name: "comma, \"quotes\", back\\slash\nnewline", bio: 'special'), + CopyAuthorsArgs.new(name: '\N', bio: 'a literal \N is not NULL') + ] + @query_sql.copy_authors(rows) + stored = @query_sql.list_authors.to_h { |a| [a.name, a.bio] } + assert_equal rows.to_h { |r| [r.name, r.bio] }, stored + end + + def test_copyfrom_many_rows_and_none + @query_sql.copy_authors([]) + assert_empty @query_sql.list_authors + @query_sql.copy_authors((1..2500).map { |i| CopyAuthorsArgs.new(name: "author #{i}", bio: nil) }) + assert_equal 2500, @query_sql.list_authors.size + end + + # ---- sqlc.embed ------------------------------------------------------------------------------------------------ + + def test_embed + first = create_author('a', 'bio a') + second = create_author('b') + @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) + @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + + rows = @query_sql.list_all_authors_books + assert_equal %w[a b], rows.map { |r| r.author.name } + assert_equal ['book 1', 'book 2'], rows.map { |r| r.book.name } + assert_equal Author.new(first, 'a', 'bio a'), rows.first.author + assert_instance_of Book, rows.first.book + assert_equal first, rows.first.book.author_id + end + + def test_embed_with_plain_columns + id = create_author('a', 'bio') + @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) + row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] + assert_equal 'the book', row.book.name + end + + def test_same_table_embedded_twice + low = create_author('same') + high = create_author('same') + create_author('different') + rows = @query_sql.get_duplicate_authors + assert_equal 1, rows.size + assert_equal [low, high], [rows.first.author.id, rows.first.author2.id] + end + + # ---- enums / sets ---------------------------------------------------------------------------------------------- + + def test_enum_and_set_columns + @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n1', bio_type: ExtendedBiosBioType::MEMOIR, + author_type: 'Author,Editor')) + @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n2', bio_type: 'Biography', author_type: nil)) + row = @query_sql.get_first_extended_bio_by_type(GetFirstExtendedBioByTypeArgs.new(bio_type: 'Memoir')) + assert_equal GetFirstExtendedBioByTypeRow.new('a', 'n1', 'Memoir', 'Author,Editor'), row + assert_equal %w[Autobiography Biography Memoir], ExtendedBiosBioType::ALL + assert_equal %w[Author Editor Translator], ExtendedBiosAuthorType::ALL + end + + # ---- transactions ---------------------------------------------------------------------------------------------- + + def test_transaction_commits_and_returns_the_block_value + result = @query_sql.transaction do |tx| + id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) + tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + :done end + assert_equal :done, result + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end - def create_second_author_and_assert - create_author_args = Mysql2Codegen::CreateAuthorArgs.new( - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - @query_sql.create_author(create_author_args) - get_author_args = Mysql2Codegen::GetAuthorArgs.new(id: 2) - actual = @query_sql.get_author(get_author_args) - expected = Mysql2Codegen::GetAuthorRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - assert_equal(expected, actual) + def test_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + raise 'boom' + end end + assert_empty @query_sql.list_authors + end - def delete_author_and_assert(id_to_delete) - delete_author_args = Mysql2Codegen::DeleteAuthorArgs.new(id: id_to_delete) - @query_sql.delete_author(delete_author_args) - actual = @query_sql.list_authors - expected = [ - Mysql2Codegen::ListAuthorsRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - ] - assert_equal(expected, actual) + def test_transaction_is_isolated_until_commit + other = QuerySql.new({}, mysql2_params) + @query_sql.transaction do |tx| + tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'uncommitted', bio: nil)) + assert_empty other.list_authors end -end \ No newline at end of file + assert_equal ['uncommitted'], other.list_authors.map(&:name) + end + + # ---- models ---------------------------------------------------------------------------------------------------- + + def test_models_are_generated_for_every_table + assert_equal %i[id name bio], Author.members + assert_equal %i[id name author_id description], Book.members + assert_equal %i[author_name name bio_type author_type], ExtendedBio.members + end +end diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index 48534f9..5c9902e 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -1,70 +1,246 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'bigdecimal' +require 'ipaddr' +require_relative 'consts' require_relative '../examples/pg/query_sql' +# End-to-end tests of the code generated for examples/authors/postgresql, against a real PostgreSQL server. class TestPg < Minitest::Test - def setup - pg_params = { - 'dbname' => ENV['DB_NAME'], - 'host' => ENV['POSTGRES_HOST'], - 'user' => ENV['DB_USER'], - 'password' => ENV['DB_PASS'] - } - @query_sql = PgCodegen::QuerySql.new({ }, pg_params) - end + include PgCodegen + + def setup + pg_params = { + 'dbname' => ENV['DB_NAME'], + 'host' => ENV['POSTGRES_HOST'], + 'port' => ENV['POSTGRES_PORT'], # optional + 'user' => ENV['DB_USER'], + 'password' => ENV['DB_PASS'] + }.compact + @query_sql = QuerySql.new({}, pg_params) + @query_sql.truncate_authors # cascades to books + @query_sql.truncate_extended_bios + @query_sql.truncate_extra_types + end + + def create_author(name, bio = nil) + @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + first = @query_sql.create_author(CreateAuthorArgs.new(name: Consts::BOJACK_AUTHOR, bio: Consts::BOJACK_THEME)) + assert_equal CreateAuthorRow.new(first.id, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), first + second = @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + + assert_equal GetAuthorRow.new(first.id, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), + @query_sql.get_author(GetAuthorArgs.new(id: first.id)) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + assert_equal [second.name, first.name].sort, @query_sql.list_authors.map(&:name).sort + + @query_sql.delete_author(DeleteAuthorArgs.new(id: first.id)) + assert_equal [ListAuthorsRow.new(second.id, Consts::DR_SEUSS_AUTHOR, Consts::DR_SEUSS_QUOTE)], + @query_sql.list_authors + end + + def test_nullable_columns_round_trip_nil + id = create_author('no bio', nil) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + end + + def test_one_on_empty_table_returns_nil + assert_nil @query_sql.test + end + + # ---- :execlastid / :execrows ----------------------------------------------------------------------------------- + + def test_execlastid_returns_the_returning_column + first = create_author('a') + second = create_author('b') + assert_kind_of Integer, first + assert_equal first + 1, second + end + + def test_execlastid_with_uuid_key + author_id = create_author('a') + book_id = @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: author_id)) + assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, book_id) + end + + def test_execrows_returns_affected_row_count + create_author('with bio', 'bio') + create_author('with bio 2', 'bio') + create_author('without bio', nil) + assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) + assert_equal 0, (@query_sql.truncate_authors; @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'x'))) + end + + # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- + + def test_named_args_limit_and_offset + %w[a b c d].each { |name| create_author(name) } + page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + assert_equal %w[b c], page.map(&:name) + end + + def test_nullable_named_arg + %w[abc abd xyz].each { |name| create_author(name) } + all = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)) + assert_equal 3, all.size + ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + assert_equal %w[abc abd], ab.map(&:name).sort + end + + # ---- array parameters (PostgreSQL has no sqlc.slice) ----------------------------------------------------------- - def test_flow - first_inserted_id = create_first_author_and_assert - create_second_author_and_assert - delete_author_and_assert(first_inserted_id) + def test_array_parameters + ids = %w[a b c].map { |name| create_author(name) } + assert_equal ids.first(2).sort, + @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(param1: ids.first(2))).map(&:id).sort + assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(param1: [])) + both = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(param1: ids, param2: %w[b c x])) + assert_equal %w[b c], both.map(&:name).sort + end + + # ---- :copyfrom ------------------------------------------------------------------------------------------------- + + def test_copyfrom + rows = [ + CopyAuthorsArgs.new(name: 'plain', bio: 'bio'), + CopyAuthorsArgs.new(name: 'no bio', bio: nil), + CopyAuthorsArgs.new(name: "tab\there, \"quotes\", back\\slash\nnewline", bio: 'special') + ] + @query_sql.copy_authors(rows) + stored = @query_sql.list_authors.to_h { |a| [a.name, a.bio] } + assert_equal rows.to_h { |r| [r.name, r.bio] }, stored + end + + def test_copyfrom_many_rows_and_none + @query_sql.copy_authors([]) + assert_empty @query_sql.list_authors + @query_sql.copy_authors((1..2500).map { |i| CopyAuthorsArgs.new(name: "author #{i}", bio: nil) }) + assert_equal 2500, @query_sql.list_authors.size + end + + # ---- sqlc.embed ------------------------------------------------------------------------------------------------ + + def test_embed + first = create_author('a', 'bio a') + second = create_author('b') + @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) + @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + + rows = @query_sql.list_all_authors_books + assert_equal %w[a b], rows.map { |r| r.author.name } + assert_equal ['book 1', 'book 2'], rows.map { |r| r.book.name } + assert_instance_of Author, rows.first.author + assert_instance_of Book, rows.first.book + assert_equal Author.new(first, 'a', 'bio a'), rows.first.author + assert_equal first, rows.first.book.author_id + end + + def test_embed_with_plain_columns + id = create_author('a', 'bio') + @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) + row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] + assert_equal 'the book', row.book.name + end + + def test_same_table_embedded_twice + low = create_author('same') + high = create_author('same') + create_author('different') + rows = @query_sql.get_duplicate_authors + assert_equal 1, rows.size + assert_equal [low, high], [rows.first.author.id, rows.first.author2.id] + end + + # ---- enums ----------------------------------------------------------------------------------------------------- + + def test_enum_column + @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n1', bio_type: ExtendedBioType::MEMOIR)) + @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n2', bio_type: ExtendedBioType::BIOGRAPHY)) + row = @query_sql.get_first_extended_bio_by_type(GetFirstExtendedBioByTypeArgs.new(bio_type: 'Biography')) + assert_equal GetFirstExtendedBioByTypeRow.new('a', 'n2', 'Biography'), row + assert_equal %w[Autobiography Biography Memoir], ExtendedBioType::ALL + end + + def test_enum_rejects_unknown_values + assert_raises(PG::InvalidTextRepresentation) do + @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n', bio_type: 'Fiction')) end + end - def create_first_author_and_assert - create_author_args = PgCodegen::CreateAuthorArgs.new( - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - @query_sql.create_author(create_author_args) - inserted_id = 1 - get_author_args = PgCodegen::GetAuthorArgs.new(id: inserted_id) - actual = @query_sql.get_author(get_author_args) - expected = PgCodegen::GetAuthorRow.new( - id: 1, - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - assert_equal(expected, actual) - inserted_id + # ---- transactions ---------------------------------------------------------------------------------------------- + + def test_transaction_commits_and_returns_the_block_value + result = @query_sql.transaction do |tx| + id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) + tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + :done end + assert_equal :done, result + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end - def create_second_author_and_assert - create_author_args = PgCodegen::CreateAuthorArgs.new( - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - @query_sql.create_author(create_author_args) - get_author_args = PgCodegen::GetAuthorArgs.new(id: 2) - actual = @query_sql.get_author(get_author_args) - expected = PgCodegen::GetAuthorRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - assert_equal(expected, actual) + def test_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + raise 'boom' + end end + assert_empty @query_sql.list_authors + end - def delete_author_and_assert(id_to_delete) - delete_author_args = PgCodegen::DeleteAuthorArgs.new(id: id_to_delete) - @query_sql.delete_author(delete_author_args) - actual = @query_sql.list_authors - expected = [ - PgCodegen::ListAuthorsRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - ] - assert_equal(expected, actual) + def test_transaction_is_isolated_until_commit + other = QuerySql.new({}, { 'dbname' => ENV['DB_NAME'], 'host' => ENV['POSTGRES_HOST'], 'port' => ENV['POSTGRES_PORT'], + 'user' => ENV['DB_USER'], 'password' => ENV['DB_PASS'] }.compact) + @query_sql.transaction do |tx| + tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'uncommitted', bio: nil)) + assert_empty other.list_authors end -end \ No newline at end of file + assert_equal ['uncommitted'], other.list_authors.map(&:name) + end + + # ---- more data types ------------------------------------------------------------------------------------------- + + def test_extra_types_round_trip + uuid = '0e52082f-bf69-480e-aff0-1a85139a1074' + time = Time.utc(2024, 5, 6, 7, 8, 9) + @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + c_uuid: uuid, c_jsonb: { 'a' => [1, 2, { 'b' => nil }] }.to_json, c_text_array: ['x', nil, 'y z'], + c_integer_array: [1, 2, 3], c_timestamptz: time, c_inet: '10.1.2.3', c_money: '12.50', c_not_null: 'nn' + )) + row = @query_sql.get_extra_types + assert_equal uuid, row.c_uuid + assert_equal({ 'a' => [1, 2, { 'b' => nil }] }, row.c_jsonb) + assert_equal ['x', nil, 'y z'], row.c_text_array + assert_equal [1, 2, 3], row.c_integer_array + assert_equal time, row.c_timestamptz.utc + assert_equal IPAddr.new('10.1.2.3'), row.c_inet + assert_includes row.c_money, '12.50' + assert_equal 'nn', row.c_not_null + end + + def test_extra_types_nulls + @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + c_uuid: nil, c_jsonb: nil, c_text_array: nil, c_integer_array: nil, c_timestamptz: nil, c_inet: nil, + c_money: nil, c_not_null: 'x' + )) + row = @query_sql.get_extra_types + assert_equal [nil] * 7, row.to_h.values_at(:c_uuid, :c_jsonb, :c_text_array, :c_integer_array, :c_timestamptz, + :c_inet, :c_money) + end + + # ---- models ---------------------------------------------------------------------------------------------------- + + def test_models_are_generated_for_every_table + assert_equal %i[id name bio], Author.members + assert_equal %i[id name author_id description], Book.members + assert_equal %i[author_name name bio_type], ExtendedBio.members + end +end diff --git a/tests/end2end_sqlite3.rb b/tests/end2end_sqlite3.rb index 50142a3..0ae91eb 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -1,71 +1,174 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'sqlite3' require_relative 'consts' require_relative '../examples/sqlite3/query_sql' +# End-to-end tests of the code generated for examples/authors/sqlite, against a real (in-memory) SQLite database. class TestSqlite3 < Minitest::Test - def setup - db_name = "#{ENV['DB_NAME']}.db" - db = SQLite3::Database.new db_name - success = system("sqlite3 #{db_name} < ./examples/authors/sqlite/schema.sql") - if success - puts "Created schema for #{db_name}" - end - @query_sql = Sqlite3Codegen::QuerySql.new(db) - end + include Sqlite3Codegen - def test_flow - first_inserted_id = create_first_author_and_assert - create_second_author_and_assert - delete_author_and_assert(first_inserted_id) - end - - def create_first_author_and_assert - create_author_args = Sqlite3Codegen::CreateAuthorArgs.new( - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - @query_sql.create_author(create_author_args) - inserted_id = 1 - get_author_args = Sqlite3Codegen::GetAuthorArgs.new(id: inserted_id) - actual = @query_sql.get_author(get_author_args) - expected = Sqlite3Codegen::GetAuthorRow.new( - id: inserted_id, - name: Consts::BOJACK_AUTHOR, - bio: Consts::BOJACK_THEME - ) - assert_equal(expected, actual) - inserted_id - end + SCHEMA = File.read(File.expand_path('../examples/authors/sqlite/schema.sql', __dir__)) + + def setup + @db = SQLite3::Database.new(':memory:') + @db.execute_batch(SCHEMA) + @query_sql = QuerySql.new(@db) + end + + def create_author(name, bio = nil) + @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) + @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + + assert_equal GetAuthorRow.new(first, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), + @query_sql.get_author(GetAuthorArgs.new(id: first)) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + assert_equal [Consts::BOJACK_AUTHOR, Consts::DR_SEUSS_AUTHOR].sort, @query_sql.list_authors.map(&:name).sort + + @query_sql.delete_author(DeleteAuthorArgs.new(id: first)) + assert_equal [Consts::DR_SEUSS_AUTHOR], @query_sql.list_authors.map(&:name) + end + + def test_nullable_columns_round_trip_nil + id = create_author('no bio', nil) + assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + end + + # ---- :execlastid / :execrows ----------------------------------------------------------------------------------- + + def test_execlastid_returns_the_returning_column + first = create_author('a') + second = create_author('b') + assert_kind_of Integer, first + assert_equal first + 1, second + assert_kind_of Integer, @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: first)) + end + + def test_execrows_returns_affected_row_count + create_author('with bio', 'bio') + create_author('with bio 2', 'bio') + create_author('without bio', nil) + assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) + end + + # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- + + def test_numbered_parameters_limit_and_offset + %w[a b c d].each { |name| create_author(name) } + page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + assert_equal %w[b c], page.map(&:name) + end + + def test_nullable_named_arg + %w[abc abd xyz].each { |name| create_author(name) } + assert_equal 3, @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)).size + ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + assert_equal %w[abc abd], ab.map(&:name).sort + end - def create_second_author_and_assert - create_author_args = Sqlite3Codegen::CreateAuthorArgs.new( - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - @query_sql.create_author(create_author_args) - get_author_args = Sqlite3Codegen::GetAuthorArgs.new(id: 2) - actual = @query_sql.get_author(get_author_args) - expected = Sqlite3Codegen::GetAuthorRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - assert_equal(expected, actual) + # ---- sqlc.slice ------------------------------------------------------------------------------------------------ + + def test_slice + ids = %w[a b c].map { |name| create_author(name) } + got = @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: ids.first(2))) + assert_equal ids.first(2).sort, got.map(&:id).sort + end + + def test_empty_slice_matches_nothing + create_author('a') + assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [])) + end + + def test_several_slices_in_one_query + ids = %w[a b c].map { |name| create_author(name) } + got = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: %w[b c x])) + assert_equal %w[b c], got.map(&:name).sort + end + + # ---- :copyfrom ------------------------------------------------------------------------------------------------- + + def test_copyfrom + rows = [ + CopyAuthorsArgs.new(name: 'plain', bio: 'bio'), + CopyAuthorsArgs.new(name: 'no bio', bio: nil), + CopyAuthorsArgs.new(name: "comma, 'quotes', \"double\", back\\slash\nnewline", bio: 'special') + ] + @query_sql.copy_authors(rows) + assert_equal rows.to_h { |r| [r.name, r.bio] }, @query_sql.list_authors.to_h { |a| [a.name, a.bio] } + end + + def test_copyfrom_is_batched_below_the_bound_parameter_limit + @query_sql.copy_authors([]) + assert_empty @query_sql.list_authors + @query_sql.copy_authors((1..2500).map { |i| CopyAuthorsArgs.new(name: "author #{i}", bio: i.to_s) }) + assert_equal 2500, @query_sql.list_authors.size + end + + # ---- sqlc.embed ------------------------------------------------------------------------------------------------ + + def test_embed + first = create_author('a', 'bio a') + second = create_author('b') + @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) + @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + + rows = @query_sql.list_all_authors_books + assert_equal %w[a b], rows.map { |r| r.author.name } + assert_equal ['book 1', 'book 2'], rows.map { |r| r.book.name } + assert_equal Author.new(first, 'a', 'bio a'), rows.first.author + assert_instance_of Book, rows.first.book + end + + def test_embed_with_plain_columns + id = create_author('a', 'bio') + @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) + row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] + assert_equal 'the book', row.book.name + end + + def test_same_table_embedded_twice + low = create_author('same') + high = create_author('same') + create_author('different') + rows = @query_sql.get_duplicate_authors + assert_equal 1, rows.size + assert_equal [low, high], [rows.first.author.id, rows.first.author2.id] + end + + # ---- transactions ---------------------------------------------------------------------------------------------- + + def test_transaction_commits_and_returns_the_block_value + result = @query_sql.transaction do |tx| + id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) + tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + :done end + assert_equal :done, result + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end - def delete_author_and_assert(id_to_delete) - delete_author_args = Sqlite3Codegen::DeleteAuthorArgs.new(id: id_to_delete) - @query_sql.delete_author(delete_author_args) - actual = @query_sql.list_authors - expected = [ - Sqlite3Codegen::ListAuthorsRow.new( - id: 2, - name: Consts::DR_SEUSS_AUTHOR, - bio: Consts::DR_SEUSS_QUOTE - ) - ] - assert_equal(expected, actual) + def test_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + raise 'boom' + end end -end \ No newline at end of file + assert_empty @query_sql.list_authors + end + + # ---- models ---------------------------------------------------------------------------------------------------- + + def test_models_are_generated_for_every_table + assert_equal %i[id name bio], Author.members + assert_equal %i[id name author_id description], Book.members + end +end From efe1b64049e83bdf4e4e015913e27ffeda592732 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 13:53:17 +0000 Subject: [PATCH 08/15] Map DB types to the Ruby classes the drivers actually return The inherited type tables did not match runtime values: numeric/decimal are BigDecimal (not Float), date is Date and timestamps/datetimes are Time (not String), blobs/bytea/bit are Strings (not Array[Integer]), json is parsed to Hash/Array (untyped), SQLite booleans are 0/1 Integers. tests/rbs_types.rb checks the values returned by the generated code against the types declared in the generated .rbs, for the example `Test` tables of all three databases (incl. all-NULL rows). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- examples/mysql2/models.rbs | 60 ++++++++-------- examples/mysql2/query_sql.rbs | 60 ++++++++-------- examples/pg/models.rbs | 28 ++++---- examples/pg/query_sql.rbs | 28 ++++---- lib/sqlc_gen_ruby/drivers/mysql2.rb | 11 +-- lib/sqlc_gen_ruby/drivers/pg.rb | 16 +++-- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 7 +- test/golden/mysql2/default/models.rbs | 60 ++++++++-------- test/golden/mysql2/default/other_sql.rbs | 4 +- test/golden/mysql2/default/query_sql.rbs | 72 +++++++++---------- test/golden/mysql2/ruby31_gemfile/models.rbs | 60 ++++++++-------- .../mysql2/ruby31_gemfile/other_sql.rbs | 4 +- .../mysql2/ruby31_gemfile/query_sql.rbs | 72 +++++++++---------- test/golden/pg/default/models.rbs | 28 ++++---- test/golden/pg/default/my_queries_sql.rb | 2 +- test/golden/pg/default/other_sql.rb | 2 +- test/golden/pg/default/other_sql.rbs | 4 +- test/golden/pg/default/query_sql.rb | 2 +- test/golden/pg/default/query_sql.rbs | 36 +++++----- test/golden/pg/ruby31_gemfile/models.rbs | 28 ++++---- .../pg/ruby31_gemfile/my_queries_sql.rb | 2 +- test/golden/pg/ruby31_gemfile/other_sql.rb | 2 +- test/golden/pg/ruby31_gemfile/other_sql.rbs | 4 +- test/golden/pg/ruby31_gemfile/query_sql.rb | 2 +- test/golden/pg/ruby31_gemfile/query_sql.rbs | 36 +++++----- .../pg/ruby32_notypes/my_queries_sql.rb | 2 +- test/golden/pg/ruby32_notypes/other_sql.rb | 2 +- test/golden/pg/ruby32_notypes/query_sql.rb | 2 +- test/golden/sqlite3/default/other_sql.rbs | 8 +-- test/golden/sqlite3/default/query_sql.rbs | 20 +++--- .../sqlite3/ruby31_gemfile/other_sql.rbs | 8 +-- .../sqlite3/ruby31_gemfile/query_sql.rbs | 20 +++--- tests/end2end_mysql2.rb | 28 ++++++++ tests/end2end_pg.rb | 44 ++++++++++++ tests/end2end_sqlite3.rb | 8 +++ tests/rbs_types.rb | 39 ++++++++++ 36 files changed, 469 insertions(+), 342 deletions(-) create mode 100644 tests/rbs_types.rb diff --git a/examples/mysql2/models.rbs b/examples/mysql2/models.rbs index 10c20c0..fd0e34d 100644 --- a/examples/mysql2/models.rbs +++ b/examples/mysql2/models.rbs @@ -22,7 +22,7 @@ module Mysql2Codegen end class NodeMysqlType - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_tinyint: Integer? attr_reader c_bool: Integer? attr_reader c_boolean: Integer? @@ -32,35 +32,35 @@ module Mysql2Codegen attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer - attr_reader c_decimal: String? - attr_reader c_dec: String? - attr_reader c_numeric: String? - attr_reader c_fixed: String? + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? attr_reader c_float: Float? attr_reader c_double: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? - attr_reader c_time: String? - attr_reader c_datetime: String? - attr_reader c_timestamp: String? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? attr_reader c_year: Integer? attr_reader c_char: String? attr_reader c_nchar: String? attr_reader c_national_char: String? attr_reader c_varchar: String? - attr_reader c_binary: Array[Integer]? - attr_reader c_varbinary: Array[Integer]? - attr_reader c_tinyblob: Array[Integer]? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? attr_reader c_tinytext: String? - attr_reader c_blob: Array[Integer]? + attr_reader c_blob: String? attr_reader c_text: String? - attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumblob: String? attr_reader c_mediumtext: String? - attr_reader c_longblob: Array[Integer]? + attr_reader c_longblob: String? attr_reader c_longtext: String? attr_reader c_json: String? def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -70,31 +70,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> void diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index d8a5904..38fb295 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -57,7 +57,7 @@ module Mysql2Codegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_tinyint: Integer? attr_reader c_bool: Integer? attr_reader c_boolean: Integer? @@ -67,35 +67,35 @@ module Mysql2Codegen attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer - attr_reader c_decimal: String? - attr_reader c_dec: String? - attr_reader c_numeric: String? - attr_reader c_fixed: String? + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? attr_reader c_float: Float? attr_reader c_double: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? - attr_reader c_time: String? - attr_reader c_datetime: String? - attr_reader c_timestamp: String? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? attr_reader c_year: Integer? attr_reader c_char: String? attr_reader c_nchar: String? attr_reader c_national_char: String? attr_reader c_varchar: String? - attr_reader c_binary: Array[Integer]? - attr_reader c_varbinary: Array[Integer]? - attr_reader c_tinyblob: Array[Integer]? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? attr_reader c_tinytext: String? - attr_reader c_blob: Array[Integer]? + attr_reader c_blob: String? attr_reader c_text: String? - attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumblob: String? attr_reader c_mediumtext: String? - attr_reader c_longblob: Array[Integer]? + attr_reader c_longblob: String? attr_reader c_longtext: String? attr_reader c_json: String? def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -105,31 +105,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> void diff --git a/examples/pg/models.rbs b/examples/pg/models.rbs index e7920f4..cec018c 100644 --- a/examples/pg/models.rbs +++ b/examples/pg/models.rbs @@ -15,43 +15,43 @@ module PgCodegen end class NodePostgresType - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_smallint: Integer? attr_reader c_boolean: bool? attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer? - attr_reader c_decimal: Float? - attr_reader c_numeric: Float? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? attr_reader c_real: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? + attr_reader c_date: Date? attr_reader c_time: String? - attr_reader c_timestamp: String? + attr_reader c_timestamp: Time? attr_reader c_char: String? attr_reader c_varchar: String? - attr_reader c_bytea: Array[Integer]? + attr_reader c_bytea: String? attr_reader c_text: String? - attr_reader c_json: String? + attr_reader c_json: untyped def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> void end diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index 537df2f..b56fa80 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -48,43 +48,43 @@ module PgCodegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_smallint: Integer? attr_reader c_boolean: bool? attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer? - attr_reader c_decimal: Float? - attr_reader c_numeric: Float? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? attr_reader c_real: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? + attr_reader c_date: Date? attr_reader c_time: String? - attr_reader c_timestamp: String? + attr_reader c_timestamp: Time? attr_reader c_char: String? attr_reader c_varchar: String? - attr_reader c_bytea: Array[Integer]? + attr_reader c_bytea: String? attr_reader c_text: String? - attr_reader c_json: String? + attr_reader c_json: untyped def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> void end diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb index d95210b..15d4bae 100644 --- a/lib/sqlc_gen_ruby/drivers/mysql2.rb +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -108,12 +108,15 @@ def copy_from?(queries) = queries.any? { |q| q.cmd == ':copyfrom' } def driver_gems = %w[mysql2] def column_mappings + # Ruby classes as returned by the mysql2 gem (verified by tests/end2end_mysql2.rb) @column_mappings ||= [ - mapping('Array[Integer]', %w[binary bit blob longblob mediumblob tinyblob varbinary]), - mapping('String', %w[char date datetime decimal longtext mediumtext text time timestamp tinytext - varchar json]), + mapping('String', %w[char varchar tinytext text mediumtext longtext json binary varbinary bit tinyblob blob + mediumblob longblob]), mapping('Integer', %w[bigint int mediumint smallint tinyint year integer]), - mapping('Float', %w[double float]) + mapping('Float', %w[double float]), + mapping('BigDecimal', %w[decimal numeric dec fixed]), + mapping('Date', %w[date]), + mapping('Time', %w[datetime timestamp time]) ] end end diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index f272ab8..69fb676 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -70,16 +70,20 @@ def copy_from_lines(query, table, columns) def driver_gems = %w[pg set] def column_mappings + # Ruby classes as returned by the pg gem with PG::BasicTypeMapForResults (verified by tests/end2end_pg.rb); + # types without a decoder (uuid, money, interval, enums, ...) come back as Strings. @column_mappings ||= [ mapping('bool', %w[bool boolean]), - mapping('Array[Integer]', %w[binary bit bytea blob longblob mediumblob tinyblob varbinary]), - mapping('String', %w[char date datetime longtext mediumtext text bpchar time timestamp tinytext - varchar json uuid money macaddr macaddr8 interval xml name citext]), + mapping('String', %w[char bpchar varchar text time uuid money macaddr macaddr8 interval xml name citext + bit varbit bytea tinytext mediumtext longtext tinyblob blob mediumblob longblob + binary varbinary]), mapping('Integer', %w[int2 int4 int8 serial bigserial integer smallint bigint int smallserial]), - mapping('Float', %w[numeric float4 float8 decimal real double]), - mapping('Time', %w[timestamptz]), + mapping('Float', %w[float4 float8 real double]), + mapping('BigDecimal', %w[numeric decimal]), + mapping('Date', %w[date]), + mapping('Time', %w[timestamp timestamptz datetime]), mapping('IPAddr', %w[inet cidr]), - mapping('untyped', %w[jsonb]) + mapping('untyped', %w[json jsonb]) ] end end diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb index ea267fb..e3c4b13 100644 --- a/lib/sqlc_gen_ruby/drivers/sqlite3.rb +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -60,10 +60,11 @@ def copy_from_lines(query, table, columns) def driver_gems = %w[sqlite3] def column_mappings + # Ruby classes as returned by the sqlite3 gem (verified by tests/end2end_sqlite3.rb): booleans are 0/1 + # Integers, blobs are Strings, dates are text. @column_mappings ||= [ - mapping('Integer', %w[int integer tinyint smallint mediumint bigint unsignedbigint int2 int8]), - mapping('bool', %w[boolean bool]), - mapping('Array[Integer]', %w[blob]), + mapping('Integer', %w[int integer tinyint smallint mediumint bigint unsignedbigint int2 int8 boolean bool]), + mapping('String', %w[blob datetime date timestamp time]), mapping('String', %w[character varchar varyingcharacter nchar nativecharacter nvarchar text clob], prefix_based: true), mapping('Float', %w[real double doubleprecision float decimal numeric], prefix_based: true) diff --git a/test/golden/mysql2/default/models.rbs b/test/golden/mysql2/default/models.rbs index 10c20c0..fd0e34d 100644 --- a/test/golden/mysql2/default/models.rbs +++ b/test/golden/mysql2/default/models.rbs @@ -22,7 +22,7 @@ module Mysql2Codegen end class NodeMysqlType - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_tinyint: Integer? attr_reader c_bool: Integer? attr_reader c_boolean: Integer? @@ -32,35 +32,35 @@ module Mysql2Codegen attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer - attr_reader c_decimal: String? - attr_reader c_dec: String? - attr_reader c_numeric: String? - attr_reader c_fixed: String? + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? attr_reader c_float: Float? attr_reader c_double: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? - attr_reader c_time: String? - attr_reader c_datetime: String? - attr_reader c_timestamp: String? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? attr_reader c_year: Integer? attr_reader c_char: String? attr_reader c_nchar: String? attr_reader c_national_char: String? attr_reader c_varchar: String? - attr_reader c_binary: Array[Integer]? - attr_reader c_varbinary: Array[Integer]? - attr_reader c_tinyblob: Array[Integer]? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? attr_reader c_tinytext: String? - attr_reader c_blob: Array[Integer]? + attr_reader c_blob: String? attr_reader c_text: String? - attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumblob: String? attr_reader c_mediumtext: String? - attr_reader c_longblob: Array[Integer]? + attr_reader c_longblob: String? attr_reader c_longtext: String? attr_reader c_json: String? def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -70,31 +70,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> void diff --git a/test/golden/mysql2/default/other_sql.rbs b/test/golden/mysql2/default/other_sql.rbs index a95ea3c..784128b 100644 --- a/test/golden/mysql2/default/other_sql.rbs +++ b/test/golden/mysql2/default/other_sql.rbs @@ -6,13 +6,13 @@ module Mysql2Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] + attr_reader c3: String attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> void end diff --git a/test/golden/mysql2/default/query_sql.rbs b/test/golden/mysql2/default/query_sql.rbs index fd46044..3530b74 100644 --- a/test/golden/mysql2/default/query_sql.rbs +++ b/test/golden/mysql2/default/query_sql.rbs @@ -57,7 +57,7 @@ module Mysql2Codegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_tinyint: Integer? attr_reader c_bool: Integer? attr_reader c_boolean: Integer? @@ -67,35 +67,35 @@ module Mysql2Codegen attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer - attr_reader c_decimal: String? - attr_reader c_dec: String? - attr_reader c_numeric: String? - attr_reader c_fixed: String? + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? attr_reader c_float: Float? attr_reader c_double: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? - attr_reader c_time: String? - attr_reader c_datetime: String? - attr_reader c_timestamp: String? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? attr_reader c_year: Integer? attr_reader c_char: String? attr_reader c_nchar: String? attr_reader c_national_char: String? attr_reader c_varchar: String? - attr_reader c_binary: Array[Integer]? - attr_reader c_varbinary: Array[Integer]? - attr_reader c_tinyblob: Array[Integer]? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? attr_reader c_tinytext: String? - attr_reader c_blob: Array[Integer]? + attr_reader c_blob: String? attr_reader c_text: String? - attr_reader c_mediumblob: Array[Integer]? + attr_reader c_mediumblob: String? attr_reader c_mediumtext: String? - attr_reader c_longblob: Array[Integer]? + attr_reader c_longblob: String? attr_reader c_longtext: String? attr_reader c_json: String? def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -105,31 +105,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> void @@ -286,13 +286,13 @@ module Mysql2Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] + attr_reader c3: String attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> void end @@ -303,13 +303,13 @@ module Mysql2Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] + attr_reader c3: String attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> void end @@ -325,8 +325,8 @@ module Mysql2Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] - def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + attr_reader c3: String + def initialize: ( Integer c0, String c1, Float c2, String c3) -> void end class QuerySql diff --git a/test/golden/mysql2/ruby31_gemfile/models.rbs b/test/golden/mysql2/ruby31_gemfile/models.rbs index 863f1fa..e3215f8 100644 --- a/test/golden/mysql2/ruby31_gemfile/models.rbs +++ b/test/golden/mysql2/ruby31_gemfile/models.rbs @@ -22,7 +22,7 @@ module Mysql2Codegen end class NodeMysqlType < ::Struct - attr_accessor c_bit: Array[Integer]? + attr_accessor c_bit: String? attr_accessor c_tinyint: Integer? attr_accessor c_bool: Integer? attr_accessor c_boolean: Integer? @@ -32,35 +32,35 @@ module Mysql2Codegen attr_accessor c_integer: Integer? attr_accessor c_bigint: Integer? attr_accessor c_serial: Integer - attr_accessor c_decimal: String? - attr_accessor c_dec: String? - attr_accessor c_numeric: String? - attr_accessor c_fixed: String? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_dec: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_fixed: BigDecimal? attr_accessor c_float: Float? attr_accessor c_double: Float? attr_accessor c_double_precision: Float? - attr_accessor c_date: String? - attr_accessor c_time: String? - attr_accessor c_datetime: String? - attr_accessor c_timestamp: String? + attr_accessor c_date: Date? + attr_accessor c_time: Time? + attr_accessor c_datetime: Time? + attr_accessor c_timestamp: Time? attr_accessor c_year: Integer? attr_accessor c_char: String? attr_accessor c_nchar: String? attr_accessor c_national_char: String? attr_accessor c_varchar: String? - attr_accessor c_binary: Array[Integer]? - attr_accessor c_varbinary: Array[Integer]? - attr_accessor c_tinyblob: Array[Integer]? + attr_accessor c_binary: String? + attr_accessor c_varbinary: String? + attr_accessor c_tinyblob: String? attr_accessor c_tinytext: String? - attr_accessor c_blob: Array[Integer]? + attr_accessor c_blob: String? attr_accessor c_text: String? - attr_accessor c_mediumblob: Array[Integer]? + attr_accessor c_mediumblob: String? attr_accessor c_mediumtext: String? - attr_accessor c_longblob: Array[Integer]? + attr_accessor c_longblob: String? attr_accessor c_longtext: String? attr_accessor c_json: String? def self.new: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -70,31 +70,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> NodeMysqlType diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs index b4a71bd..cda3807 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs @@ -6,13 +6,13 @@ module Mysql2Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] + attr_accessor c3: String attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> NoColsArgs end diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs index de5a5da..0a5fd89 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -57,7 +57,7 @@ module Mysql2Codegen TestSql: String class TestRow < ::Struct - attr_accessor c_bit: Array[Integer]? + attr_accessor c_bit: String? attr_accessor c_tinyint: Integer? attr_accessor c_bool: Integer? attr_accessor c_boolean: Integer? @@ -67,35 +67,35 @@ module Mysql2Codegen attr_accessor c_integer: Integer? attr_accessor c_bigint: Integer? attr_accessor c_serial: Integer - attr_accessor c_decimal: String? - attr_accessor c_dec: String? - attr_accessor c_numeric: String? - attr_accessor c_fixed: String? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_dec: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_fixed: BigDecimal? attr_accessor c_float: Float? attr_accessor c_double: Float? attr_accessor c_double_precision: Float? - attr_accessor c_date: String? - attr_accessor c_time: String? - attr_accessor c_datetime: String? - attr_accessor c_timestamp: String? + attr_accessor c_date: Date? + attr_accessor c_time: Time? + attr_accessor c_datetime: Time? + attr_accessor c_timestamp: Time? attr_accessor c_year: Integer? attr_accessor c_char: String? attr_accessor c_nchar: String? attr_accessor c_national_char: String? attr_accessor c_varchar: String? - attr_accessor c_binary: Array[Integer]? - attr_accessor c_varbinary: Array[Integer]? - attr_accessor c_tinyblob: Array[Integer]? + attr_accessor c_binary: String? + attr_accessor c_varbinary: String? + attr_accessor c_tinyblob: String? attr_accessor c_tinytext: String? - attr_accessor c_blob: Array[Integer]? + attr_accessor c_blob: String? attr_accessor c_text: String? - attr_accessor c_mediumblob: Array[Integer]? + attr_accessor c_mediumblob: String? attr_accessor c_mediumtext: String? - attr_accessor c_longblob: Array[Integer]? + attr_accessor c_longblob: String? attr_accessor c_longtext: String? attr_accessor c_json: String? def self.new: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_tinyint, Integer? c_bool, Integer? c_boolean, @@ -105,31 +105,31 @@ module Mysql2Codegen Integer? c_integer, Integer? c_bigint, Integer c_serial, - String? c_decimal, - String? c_dec, - String? c_numeric, - String? c_fixed, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, Float? c_float, Float? c_double, Float? c_double_precision, - String? c_date, - String? c_time, - String? c_datetime, - String? c_timestamp, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, Integer? c_year, String? c_char, String? c_nchar, String? c_national_char, String? c_varchar, - Array[Integer]? c_binary, - Array[Integer]? c_varbinary, - Array[Integer]? c_tinyblob, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, String? c_tinytext, - Array[Integer]? c_blob, + String? c_blob, String? c_text, - Array[Integer]? c_mediumblob, + String? c_mediumblob, String? c_mediumtext, - Array[Integer]? c_longblob, + String? c_longblob, String? c_longtext, String? c_json ) -> TestRow @@ -286,13 +286,13 @@ module Mysql2Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] + attr_accessor c3: String attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> ManyParamsArgs end @@ -303,13 +303,13 @@ module Mysql2Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] + attr_accessor c3: String attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, + String c3, Integer c4 ) -> ManyRowsRow end @@ -325,8 +325,8 @@ module Mysql2Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] - def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + attr_accessor c3: String + def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow end class QuerySql diff --git a/test/golden/pg/default/models.rbs b/test/golden/pg/default/models.rbs index e7920f4..cec018c 100644 --- a/test/golden/pg/default/models.rbs +++ b/test/golden/pg/default/models.rbs @@ -15,43 +15,43 @@ module PgCodegen end class NodePostgresType - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_smallint: Integer? attr_reader c_boolean: bool? attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer? - attr_reader c_decimal: Float? - attr_reader c_numeric: Float? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? attr_reader c_real: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? + attr_reader c_date: Date? attr_reader c_time: String? - attr_reader c_timestamp: String? + attr_reader c_timestamp: Time? attr_reader c_char: String? attr_reader c_varchar: String? - attr_reader c_bytea: Array[Integer]? + attr_reader c_bytea: String? attr_reader c_text: String? - attr_reader c_json: String? + attr_reader c_json: untyped def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> void end diff --git a/test/golden/pg/default/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb index 14da5d7..88c372a 100644 --- a/test/golden/pg/default/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -16,7 +16,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/default/other_sql.rb b/test/golden/pg/default/other_sql.rb index 33e656e..a81600c 100644 --- a/test/golden/pg/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -34,7 +34,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/default/other_sql.rbs b/test/golden/pg/default/other_sql.rbs index 2a7bc6f..e287a6c 100644 --- a/test/golden/pg/default/other_sql.rbs +++ b/test/golden/pg/default/other_sql.rbs @@ -7,13 +7,13 @@ module PgCodegen attr_reader c1: Integer attr_reader c2: String attr_reader c3: Float - attr_reader c4: Array[Integer] + attr_reader c4: String def initialize: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> void end diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb index 7c250a0..f832ff3 100644 --- a/test/golden/pg/default/query_sql.rb +++ b/test/golden/pg/default/query_sql.rb @@ -249,7 +249,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs index c6fed01..7b5f549 100644 --- a/test/golden/pg/default/query_sql.rbs +++ b/test/golden/pg/default/query_sql.rbs @@ -48,43 +48,43 @@ module PgCodegen TestSql: String class TestRow - attr_reader c_bit: Array[Integer]? + attr_reader c_bit: String? attr_reader c_smallint: Integer? attr_reader c_boolean: bool? attr_reader c_integer: Integer? attr_reader c_bigint: Integer? attr_reader c_serial: Integer? - attr_reader c_decimal: Float? - attr_reader c_numeric: Float? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? attr_reader c_real: Float? attr_reader c_double_precision: Float? - attr_reader c_date: String? + attr_reader c_date: Date? attr_reader c_time: String? - attr_reader c_timestamp: String? + attr_reader c_timestamp: Time? attr_reader c_char: String? attr_reader c_varchar: String? - attr_reader c_bytea: Array[Integer]? + attr_reader c_bytea: String? attr_reader c_text: String? - attr_reader c_json: String? + attr_reader c_json: untyped def initialize: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> void end @@ -304,13 +304,13 @@ module PgCodegen attr_reader c1: Integer attr_reader c2: String attr_reader c3: Float - attr_reader c4: Array[Integer] + attr_reader c4: String def initialize: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> void end @@ -321,13 +321,13 @@ module PgCodegen attr_reader c1: Integer attr_reader c2: String attr_reader c3: Float - attr_reader c4: Array[Integer] + attr_reader c4: String def initialize: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> void end diff --git a/test/golden/pg/ruby31_gemfile/models.rbs b/test/golden/pg/ruby31_gemfile/models.rbs index 403270a..2912850 100644 --- a/test/golden/pg/ruby31_gemfile/models.rbs +++ b/test/golden/pg/ruby31_gemfile/models.rbs @@ -15,43 +15,43 @@ module PgCodegen end class NodePostgresType < ::Struct - attr_accessor c_bit: Array[Integer]? + attr_accessor c_bit: String? attr_accessor c_smallint: Integer? attr_accessor c_boolean: bool? attr_accessor c_integer: Integer? attr_accessor c_bigint: Integer? attr_accessor c_serial: Integer? - attr_accessor c_decimal: Float? - attr_accessor c_numeric: Float? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_numeric: BigDecimal? attr_accessor c_real: Float? attr_accessor c_double_precision: Float? - attr_accessor c_date: String? + attr_accessor c_date: Date? attr_accessor c_time: String? - attr_accessor c_timestamp: String? + attr_accessor c_timestamp: Time? attr_accessor c_char: String? attr_accessor c_varchar: String? - attr_accessor c_bytea: Array[Integer]? + attr_accessor c_bytea: String? attr_accessor c_text: String? - attr_accessor c_json: String? + attr_accessor c_json: untyped def self.new: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> NodePostgresType end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb index eebbd37..61b0d85 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -16,7 +16,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb index 7241699..1b58d8a 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -34,7 +34,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rbs b/test/golden/pg/ruby31_gemfile/other_sql.rbs index 6a9b51a..204d826 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -7,13 +7,13 @@ module PgCodegen attr_accessor c1: Integer attr_accessor c2: String attr_accessor c3: Float - attr_accessor c4: Array[Integer] + attr_accessor c4: String def self.new: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> NoColsArgs end diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb index 5d6fca5..27adf22 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rb +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -249,7 +249,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs index 9fd1201..bd5c2b4 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -48,43 +48,43 @@ module PgCodegen TestSql: String class TestRow < ::Struct - attr_accessor c_bit: Array[Integer]? + attr_accessor c_bit: String? attr_accessor c_smallint: Integer? attr_accessor c_boolean: bool? attr_accessor c_integer: Integer? attr_accessor c_bigint: Integer? attr_accessor c_serial: Integer? - attr_accessor c_decimal: Float? - attr_accessor c_numeric: Float? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_numeric: BigDecimal? attr_accessor c_real: Float? attr_accessor c_double_precision: Float? - attr_accessor c_date: String? + attr_accessor c_date: Date? attr_accessor c_time: String? - attr_accessor c_timestamp: String? + attr_accessor c_timestamp: Time? attr_accessor c_char: String? attr_accessor c_varchar: String? - attr_accessor c_bytea: Array[Integer]? + attr_accessor c_bytea: String? attr_accessor c_text: String? - attr_accessor c_json: String? + attr_accessor c_json: untyped def self.new: ( - Array[Integer]? c_bit, + String? c_bit, Integer? c_smallint, bool? c_boolean, Integer? c_integer, Integer? c_bigint, Integer? c_serial, - Float? c_decimal, - Float? c_numeric, + BigDecimal? c_decimal, + BigDecimal? c_numeric, Float? c_real, Float? c_double_precision, - String? c_date, + Date? c_date, String? c_time, - String? c_timestamp, + Time? c_timestamp, String? c_char, String? c_varchar, - Array[Integer]? c_bytea, + String? c_bytea, String? c_text, - String? c_json + untyped c_json ) -> TestRow end @@ -304,13 +304,13 @@ module PgCodegen attr_accessor c1: Integer attr_accessor c2: String attr_accessor c3: Float - attr_accessor c4: Array[Integer] + attr_accessor c4: String def self.new: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> ManyParamsArgs end @@ -321,13 +321,13 @@ module PgCodegen attr_accessor c1: Integer attr_accessor c2: String attr_accessor c3: Float - attr_accessor c4: Array[Integer] + attr_accessor c4: String def self.new: ( bool c0, Integer c1, String c2, Float c3, - Array[Integer] c4 + String c4 ) -> ManyRowsRow end diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb index 14da5d7..88c372a 100644 --- a/test/golden/pg/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -16,7 +16,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb index 33e656e..a81600c 100644 --- a/test/golden/pg/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -34,7 +34,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb index 7c250a0..f832ff3 100644 --- a/test/golden/pg/ruby32_notypes/query_sql.rb +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -249,7 +249,7 @@ def initialize(connection_pool_params, pg_params) @db = ConnectionPool.new(**connection_pool_params) { client = PG.connect(**pg_params) type_map = PG::BasicTypeMapForResults.new client - type_map.add_coder(PG::TextDecoder::String.new(oid: 2950, name: 'uuid')) # uuid has no default decoder + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings client.type_map_for_results = type_map client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client diff --git a/test/golden/sqlite3/default/other_sql.rbs b/test/golden/sqlite3/default/other_sql.rbs index 06c3bbd..a9ea16b 100644 --- a/test/golden/sqlite3/default/other_sql.rbs +++ b/test/golden/sqlite3/default/other_sql.rbs @@ -6,14 +6,14 @@ module Sqlite3Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: bool + attr_reader c3: String + attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> void end diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs index d5a2758..30d55b4 100644 --- a/test/golden/sqlite3/default/query_sql.rbs +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -180,14 +180,14 @@ module Sqlite3Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: bool + attr_reader c3: String + attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> void end @@ -197,14 +197,14 @@ module Sqlite3Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] - attr_reader c4: bool + attr_reader c3: String + attr_reader c4: Integer def initialize: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> void end @@ -219,8 +219,8 @@ module Sqlite3Codegen attr_reader c0: Integer attr_reader c1: String attr_reader c2: Float - attr_reader c3: Array[Integer] - def initialize: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> void + attr_reader c3: String + def initialize: ( Integer c0, String c1, Float c2, String c3) -> void end class QuerySql diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs index 64ee08f..1f61cf2 100644 --- a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs @@ -6,14 +6,14 @@ module Sqlite3Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: bool + attr_accessor c3: String + attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> NoColsArgs end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs index d6d7efc..ea35794 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -180,14 +180,14 @@ module Sqlite3Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: bool + attr_accessor c3: String + attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> ManyParamsArgs end @@ -197,14 +197,14 @@ module Sqlite3Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] - attr_accessor c4: bool + attr_accessor c3: String + attr_accessor c4: Integer def self.new: ( Integer c0, String c1, Float c2, - Array[Integer] c3, - bool c4 + String c3, + Integer c4 ) -> ManyRowsRow end @@ -219,8 +219,8 @@ module Sqlite3Codegen attr_accessor c0: Integer attr_accessor c1: String attr_accessor c2: Float - attr_accessor c3: Array[Integer] - def self.new: ( Integer c0, String c1, Float c2, Array[Integer] c3) -> FourColsRow + attr_accessor c3: String + def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow end class QuerySql diff --git a/tests/end2end_mysql2.rb b/tests/end2end_mysql2.rb index 305bd45..1fcc49a 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -2,6 +2,7 @@ gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' require_relative 'consts' +require_relative 'rbs_types' require_relative '../examples/mysql2/query_sql' # End-to-end tests of the code generated for examples/authors/mysql, against a real MySQL / MariaDB server. @@ -212,6 +213,33 @@ def test_transaction_is_isolated_until_commit assert_equal ['uncommitted'], other.list_authors.map(&:name) end + def test_declared_types_match_runtime_values + client = Mysql2::Client.new(mysql2_params) + client.query('DELETE FROM node_mysql_types') + client.query(<<~SQL) + INSERT INTO node_mysql_types VALUES (b'1', 1, 1, 0, 2, 3, 4, 5, 6, 7, 1.5, 2.5, 3.5, 4.5, 1.25, 2.25, 3.25, + '2024-01-02', '03:04:05', '2024-01-02 03:04:05', '2024-01-02 03:04:05', 2024, 'a', 'b', 'c', 'dd', 'x', 'yy', + 'tb', 'tt', 'bl', 'te', 'mb', 'mt', 'lb', 'lt', '{"a": 1}') + SQL + row = @query_sql.test + assert_empty RbsTypes.mismatches(row, File.expand_path('../examples/mysql2/query_sql.rbs', __dir__), 'TestRow') + assert_equal Date.new(2024, 1, 2), row.c_date + assert_equal BigDecimal('1.5'), row.c_decimal + assert_equal 2024, row.c_year + ensure + client&.query('DELETE FROM node_mysql_types') + end + + def test_declared_types_match_runtime_values_for_nulls + client = Mysql2::Client.new(mysql2_params) + client.query('DELETE FROM node_mysql_types') + client.query('INSERT INTO node_mysql_types (c_int) VALUES (NULL)') + row = @query_sql.test + assert_empty RbsTypes.mismatches(row, File.expand_path('../examples/mysql2/query_sql.rbs', __dir__), 'TestRow') + ensure + client&.query('DELETE FROM node_mysql_types') + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index 5c9902e..ee2e622 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -3,7 +3,9 @@ require 'minitest/autorun' require 'bigdecimal' require 'ipaddr' +require 'securerandom' require_relative 'consts' +require_relative 'rbs_types' require_relative '../examples/pg/query_sql' # End-to-end tests of the code generated for examples/authors/postgresql, against a real PostgreSQL server. @@ -236,6 +238,48 @@ def test_extra_types_nulls :c_inet, :c_money) end + def raw_client + PG.connect(dbname: ENV['DB_NAME'], host: ENV['POSTGRES_HOST'], port: ENV['POSTGRES_PORT'], user: ENV['DB_USER'], + password: ENV['DB_PASS']) + end + + def test_declared_types_match_runtime_values + client = raw_client + client.exec('DELETE FROM node_postgres_types') + client.exec(<<~SQL) + INSERT INTO node_postgres_types VALUES (B'1', 2, true, 4, 5, 6, 1.5, 2.5, 1.25, 2.25, '2024-01-02', '03:04:05', + '2024-01-02 03:04:05', 'a', 'b', '\\x0102', 'c', '{"a": 1}') + SQL + row = @query_sql.test + assert_empty RbsTypes.mismatches(row, File.expand_path('../examples/pg/query_sql.rbs', __dir__), 'TestRow') + assert_equal Date.new(2024, 1, 2), row.c_date + assert_equal BigDecimal('1.5'), row.c_decimal + assert_equal({ 'a' => 1 }, row.c_json) + assert_equal "\x01\x02".b, row.c_bytea.b + ensure + client&.exec('DELETE FROM node_postgres_types') + end + + def test_declared_types_match_runtime_values_for_nulls + client = raw_client + client.exec('DELETE FROM node_postgres_types') + client.exec('INSERT INTO node_postgres_types (c_integer) VALUES (NULL)') + row = @query_sql.test + assert_empty RbsTypes.mismatches(row, File.expand_path('../examples/pg/query_sql.rbs', __dir__), 'TestRow') + assert_equal [nil], row.to_h.except(:c_serial).values.uniq # c_serial is filled by its default + ensure + client&.exec('DELETE FROM node_postgres_types') + end + + def test_extra_types_match_declared_types + @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + c_uuid: SecureRandom.uuid, c_jsonb: '[1]', c_text_array: %w[a], c_integer_array: [1], c_timestamptz: Time.now, + c_inet: '::1', c_money: '1', c_not_null: 'x' + )) + assert_empty RbsTypes.mismatches(@query_sql.get_extra_types, File.expand_path('../examples/pg/query_sql.rbs', __dir__), + 'GetExtraTypesRow') + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_sqlite3.rb b/tests/end2end_sqlite3.rb index 0ae91eb..8536c88 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -3,6 +3,7 @@ require 'minitest/autorun' require 'sqlite3' require_relative 'consts' +require_relative 'rbs_types' require_relative '../examples/sqlite3/query_sql' # End-to-end tests of the code generated for examples/authors/sqlite, against a real (in-memory) SQLite database. @@ -165,6 +166,13 @@ def test_transaction_rolls_back_on_error assert_empty @query_sql.list_authors end + def test_declared_types_match_runtime_values + @db.execute('INSERT INTO authors (id, name, bio) VALUES (1, ?, ?)', ['a', 'b']) + @db.execute('INSERT INTO authors (id, name, bio) VALUES (2, ?, NULL)', ['c']) + rbs = File.expand_path('../examples/sqlite3/query_sql.rbs', __dir__) + @query_sql.list_authors.each { |row| assert_empty RbsTypes.mismatches(row, rbs, 'ListAuthorsRow') } + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/rbs_types.rb b/tests/rbs_types.rb new file mode 100644 index 0000000..23d2b10 --- /dev/null +++ b/tests/rbs_types.rb @@ -0,0 +1,39 @@ +require 'bigdecimal' +require 'date' +require 'ipaddr' +require 'time' + +# Checks that the values the generated code returns are instances of the types the generated .rbs declares +# for them (dependency free: reads `attr_reader name: Type` lines instead of loading the signatures). +module RbsTypes + module_function + + # { 'c_date' => 'Date?', ... } for `class ` in an .rbs file + def attributes(rbs_path, class_name) + body = File.read(rbs_path)[/^\t+class #{class_name}\n(.*?)^\t+end$/m, 1] or raise "#{class_name} not in #{rbs_path}" + body.scan(/attr_(?:reader|accessor) (\w+): (.+)$/).to_h + end + + # Returns the list of mismatches (empty when every attribute value has its declared type). + def mismatches(row, rbs_path, class_name) + attributes(rbs_path, class_name).filter_map do |name, type| + value = row.public_send(name) + next if matches?(value, type) + + "#{class_name}##{name}: declared #{type}, got #{value.class} (#{value.inspect[0, 40]})" + end + end + + def matches?(value, type) + optional = type.end_with?('?') + type = type.chomp('?') + return true if type == 'untyped' + return optional if value.nil? + + case type + when 'bool' then [true, false].include?(value) + when /\AArray\[(.+)\]\z/ then value.is_a?(Array) && value.all? { |v| matches?(v, "#{Regexp.last_match(1)}?") } + else value.is_a?(Object.const_get(type)) + end + end +end From a583b74b193ad6419815cd89a34f254b27c27e54 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 14:06:03 +0000 Subject: [PATCH 09/15] Fix PostgreSQL prepared statements with pools > 1, document features, Steep for the new output - PG: prepared statements were remembered per query object, but they live on a connection. With a connection pool larger than one the second connection failed with "prepared statement does not exist". Track them per connection; tests/end2end_pg.rb reproduces the failure on the old output with three pooled connections. - README: supported-features matrix (annotations, macros, models, transactions), `overrides` option, MySQL copyfrom requirements; docs/Design.md: limitations - docker-compose: MySQL started with --local-infile=1 (needed by :copyfrom) - Steepfile loads the stdlib signatures used by the generated types; steep_expectations.yml regenerated with the locked Steep 1.6.0 (only the unknown-gem-constant warnings remain) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- README.md | 44 ++++++ Steepfile | 5 +- docker-compose.yml | 1 + docs/Design.md | 22 ++- examples/pg/query_sql.rb | 112 ++++++++------- examples/pg/query_sql.rbs | 2 +- lib/sqlc_gen_ruby/drivers/pg.rb | 10 +- steep_expectations.yml | 60 +++++++-- test/golden/pg/default/my_queries_sql.rb | 7 +- test/golden/pg/default/my_queries_sql.rbs | 2 +- test/golden/pg/default/other_sql.rb | 17 ++- test/golden/pg/default/other_sql.rbs | 2 +- test/golden/pg/default/query_sql.rb | 127 +++++++++++------- test/golden/pg/default/query_sql.rbs | 2 +- .../pg/ruby31_gemfile/my_queries_sql.rb | 7 +- .../pg/ruby31_gemfile/my_queries_sql.rbs | 2 +- test/golden/pg/ruby31_gemfile/other_sql.rb | 17 ++- test/golden/pg/ruby31_gemfile/other_sql.rbs | 2 +- test/golden/pg/ruby31_gemfile/query_sql.rb | 127 +++++++++++------- test/golden/pg/ruby31_gemfile/query_sql.rbs | 2 +- .../pg/ruby32_notypes/my_queries_sql.rb | 7 +- test/golden/pg/ruby32_notypes/other_sql.rb | 17 ++- test/golden/pg/ruby32_notypes/query_sql.rb | 127 +++++++++++------- tests/end2end_pg.rb | 38 +++++- 24 files changed, 500 insertions(+), 259 deletions(-) diff --git a/README.md b/README.md index 97a447a..5138fc3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,20 @@ codec), and it is shipped as a single self-contained WebAssembly module — the | rubyVersion | default: `3.3`
values: `3.1`, `3.2`, `3.3` | Yes | Determines the Ruby version the generated code should support.. | | generateGemfile | default: `false`
values: `false`,`true` | Yes | Assists you with the integration of SQLC and Ruby by generating a `Gemfile` with the needed dependencies. | | generateTypes | default: `true`
values: `false`,`true` | Yes | Determines whether to generate type signatures in addition to the code. | +| overrides | a list, see [below](#override-option) | Yes | Overrides the generated (RBS) type of specific columns / parameters. | + +### Override option +```yaml +overrides: + - column: ":" # one query, e.g. "GetAuthor:bio" + ruby_type: + type: "Symbol" + notNull: true + - column: "*:" # every query + ruby_type: + type: "Pathname" + notNull: false +``` ### Configuration ```yaml @@ -67,6 +81,36 @@ sql: ### [Generated Code Examples](docs/Examples.md) +## Supported features +✅ supported · 🚫 not offered by the database / sqlc + +### Query annotations +| Annotation | PostgreSQL | MySQL | SQLite | Notes | +|---------------|:---:|:---:|:---:|-------| +| `:one` | ✅ | ✅ | ✅ | returns the row or `nil` | +| `:many` | ✅ | ✅ | ✅ | returns an `Array` of rows | +| `:exec` | ✅ | ✅ | ✅ | | +| `:execrows` | ✅ | ✅ | ✅ | returns the number of affected rows | +| `:execlastid` | ✅ | ✅ | ✅ | PostgreSQL / SQLite need a `RETURNING ` clause (the value of that column is returned); MySQL returns `client.last_id` | +| `:copyfrom` | ✅ | ✅ | ✅ | takes an `Array` of `Args`. PostgreSQL: `COPY ... FROM STDIN`. MySQL: `LOAD DATA LOCAL INFILE` from a temporary CSV - needs `local_infile: true` in the `Mysql2::Client` params and `local_infile=1` on the server. SQLite: batched multi-row `INSERT` | + +### Macros +| Macro | PostgreSQL | MySQL | SQLite | Notes | +|---------------|:---:|:---:|:---:|-------| +| `sqlc.arg` | ✅ | ✅ | ✅ | names the attribute of the `Args` class (anonymous parameters such as `ANY($1::BIGINT[])` are called `param1`, `param2`, ...) | +| `sqlc.narg` | ✅ | ✅ | ✅ | nullable parameter, typed `T?` | +| `sqlc.slice` | 🚫 | ✅ | ✅ | the `IN (...)` list is expanded at runtime (an empty list matches nothing). PostgreSQL: pass an array to `= ANY($1::T[])` instead | +| `sqlc.embed` | ✅ | ✅ | ✅ | the row gets an attribute holding the table's model (`author`, `author2` for the same table twice) | + +### Other +* **Models:** a `models.rb` / `models.rbs` with one `Data` (`Struct` below Ruby 3.2) class per table, and one module with constants per enum (`ExtendedBioType::MEMOIR`, `ExtendedBioType::ALL`). Enum columns are `String`s. +* **Transactions:** `queries.transaction { |tx| tx.create_author(...) }` runs everything in the block on one connection in a transaction (commit when the block returns, rollback when it raises) and returns the block's value. +* **Nullability:** nullable columns / parameters are typed `T?` in the `.rbs`. +* **Arrays (PostgreSQL):** `Array[T]` for columns and parameters. +* Types follow the Ruby classes the drivers actually return (e.g. `BigDecimal` for `numeric`/`decimal`, `Date`, `Time`, `IPAddr`; `json`/`jsonb` are parsed, so `untyped`). + +Not applicable to the Ruby plugin (they exist in the C# plugin because of its runtime): `withAsyncSuffix`, `withCancellationToken`, `useDapper`, target framework / csproj / namespace options. + # Contributing ## Local plugin development ### Prerequisites diff --git a/Steepfile b/Steepfile index d1d22e9..de52a99 100644 --- a/Steepfile +++ b/Steepfile @@ -1,4 +1,7 @@ target :gen do check "examples" signature "examples" -end \ No newline at end of file + + # stdlib signatures for the types the generated code refers to (BigDecimal, Date, Time, IPAddr, Set, Tempfile; CSV has no useful signature) + library "bigdecimal", "date", "time", "ipaddr", "set", "tempfile" +end diff --git a/docker-compose.yml b/docker-compose.yml index 66bd39d..af3a544 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: mysqldb: container_name: mysqldb build: ./examples/authors/mysql + command: --local-infile=1 # :copyfrom uses LOAD DATA LOCAL INFILE restart: always ports: - "3306:3306" diff --git a/docs/Design.md b/docs/Design.md index 46f49b3..39a6185 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -11,8 +11,10 @@ process or as a WASI module. This plugin is pure Ruby and is distributed as a WA | `lib/sqlc_gen_ruby/proto.rb` | ~150 line protobuf wire codec. `google-protobuf` is a C extension and cannot be loaded inside ruby.wasm, so the plugin brings its own | | `lib/sqlc_gen_ruby/plugin_pb.rb` | the subset of sqlc's `codegen.proto` the generator needs (field numbers must match upstream) | | `lib/sqlc_gen_ruby/options.rb`, `ruby_version.rb` | the `options:` JSON, `rubyVersion` parsing / validation (3.1 – 3.3; `Struct` below 3.2, `Data` from 3.2) | -| `lib/sqlc_gen_ruby/generator.rb` | module / class / dataclass (`Data.define`, `Struct.new`) layout, `.rb` / `.rbs` / `Gemfile` files | -| `lib/sqlc_gen_ruby/method_gen.rb` | one generated method per query: `:one`, `:many`, `:exec`, `:execlastid` | +| `lib/sqlc_gen_ruby/generator.rb` | module / class / dataclass (`Data.define`, `Struct.new`) layout, `.rb` / `.rbs` / `Gemfile` files, `models.rb` (tables + enums) | +| `lib/sqlc_gen_ruby/method_gen.rb` | one generated method per query: `:one`, `:many`, `:exec`, `:execrows`, `:execlastid`, `:copyfrom`, `sqlc.slice` | +| `lib/sqlc_gen_ruby/row_layout.rb` | how result columns become `Row` attributes (`sqlc.embed` expands a table into one model attribute) | +| `lib/sqlc_gen_ruby/type_resolver.rb`, `catalog.rb` | RBS type of a column (DB type mapping + array + nullability + `overrides:`), tables / enums of the catalog | | `lib/sqlc_gen_ruby/drivers/*.rb` | `Base` + `Pg`, `Mysql2`, `Sqlite3`: column type mapping, statement preparation / execution, pooling, row access | | `lib/sqlc_gen_ruby/code.rb` | indent / snake_case helpers (formatting must stay byte-identical with the original implementation) | @@ -44,7 +46,21 @@ The previous C# implementation defined the expected behaviour. Its output is kep `test/codec_test.rb` cross-checks the hand-written protobuf codec against Google's official implementation (test-only dependency). -## Known quirks kept for output compatibility +## Beyond the C# port +The first version of the Ruby plugin reproduced the C# plugin's output exactly (verified against the C# binary, see git +history). It has since gained the sqlc features of the sibling `sqlc-gen-csharp` plugin (`:execrows`, `:execlastid` for +every driver, `:copyfrom`, `sqlc.slice`, `sqlc.embed`, enums, models, transactions, nullability, `overrides`) and +its type tables were corrected to the classes the drivers really return, so the output is no longer identical. +`tests/end2end_*.rb` run the generated code against real databases; `tests/rbs_types.rb` checks returned values +against the generated signatures. + +## Known limitations +* `%q(...)` SQL constants break if the query text has unbalanced parentheses (inside a string literal). +* The SQLite `NUMERIC` affinity can return Integers where `Float` is declared. +* `:copyfrom` for MySQL writes binary (`BLOB`) values through a text CSV; only text-safe values round-trip. +* The generated query classes are not thread-safe for SQLite (a single connection is injected). + +## Known quirks kept from the C# implementation * With Ruby < 3.2 the generated `Struct.new(id, name)` lists bare identifiers, not symbols (`:id, :name`). This is what the C# plugin generated, so it is preserved. * `3.3.x` (a patch version) is rejected: the original version comparison treats a patch as greater than "no patch". diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index 424f21b..b016b7b 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -218,7 +218,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -232,9 +232,10 @@ def transaction def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author') client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') + prepared.add('get_author') end result = client.exec_prepared('get_author', query_params) row = result.first @@ -246,9 +247,10 @@ def get_author(get_author_args) def list_authors @db.with do |client| - unless @prepared_statements.include?('list_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors') client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') + prepared.add('list_authors') end result = client.exec_prepared('list_authors') entities = [] @@ -262,9 +264,10 @@ def list_authors def create_author(create_author_args) @db.with do |client| query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author') client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') + prepared.add('create_author') end result = client.exec_prepared('create_author', query_params) row = result.first @@ -277,9 +280,10 @@ def create_author(create_author_args) def delete_author(delete_author_args) @db.with do |client| query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('delete_author') client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') + prepared.add('delete_author') end client.exec_prepared('delete_author', query_params) end @@ -287,9 +291,10 @@ def delete_author(delete_author_args) def test @db.with do |client| - unless @prepared_statements.include?('test') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('test') client.prepare('test', TestSql) - @prepared_statements.add('test') + prepared.add('test') end result = client.exec_prepared('test') row = result.first @@ -321,9 +326,10 @@ def test def list_authors_paged(list_authors_paged_args) @db.with do |client| query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - unless @prepared_statements.include?('list_authors_paged') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors_paged') client.prepare('list_authors_paged', ListAuthorsPagedSql) - @prepared_statements.add('list_authors_paged') + prepared.add('list_authors_paged') end result = client.exec_prepared('list_authors_paged', query_params) entities = [] @@ -337,9 +343,10 @@ def list_authors_paged(list_authors_paged_args) def get_author_by_name_pattern(get_author_by_name_pattern_args) @db.with do |client| query_params = [get_author_by_name_pattern_args.name_pattern] - unless @prepared_statements.include?('get_author_by_name_pattern') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author_by_name_pattern') client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - @prepared_statements.add('get_author_by_name_pattern') + prepared.add('get_author_by_name_pattern') end result = client.exec_prepared('get_author_by_name_pattern', query_params) entities = [] @@ -353,9 +360,10 @@ def get_author_by_name_pattern(get_author_by_name_pattern_args) def create_author_return_id(create_author_return_id_args) @db.with do |client| query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - unless @prepared_statements.include?('create_author_return_id') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author_return_id') client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - @prepared_statements.add('create_author_return_id') + prepared.add('create_author_return_id') end result = client.exec_prepared('create_author_return_id', query_params) return result.first['id'] @@ -365,9 +373,10 @@ def create_author_return_id(create_author_return_id_args) def update_author_bios(update_author_bios_args) @db.with do |client| query_params = [update_author_bios_args.bio] - unless @prepared_statements.include?('update_author_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('update_author_bios') client.prepare('update_author_bios', UpdateAuthorBiosSql) - @prepared_statements.add('update_author_bios') + prepared.add('update_author_bios') end result = client.exec_prepared('update_author_bios', query_params) return result.cmd_tuples @@ -377,9 +386,10 @@ def update_author_bios(update_author_bios_args) def get_authors_by_ids(get_authors_by_ids_args) @db.with do |client| query_params = [get_authors_by_ids_args.param1] - unless @prepared_statements.include?('get_authors_by_ids') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids') client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - @prepared_statements.add('get_authors_by_ids') + prepared.add('get_authors_by_ids') end result = client.exec_prepared('get_authors_by_ids', query_params) entities = [] @@ -393,9 +403,10 @@ def get_authors_by_ids(get_authors_by_ids_args) def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) @db.with do |client| query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - unless @prepared_statements.include?('get_authors_by_ids_and_names') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids_and_names') client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - @prepared_statements.add('get_authors_by_ids_and_names') + prepared.add('get_authors_by_ids_and_names') end result = client.exec_prepared('get_authors_by_ids_and_names', query_params) entities = [] @@ -419,9 +430,10 @@ def copy_authors(copy_authors_args) def truncate_authors @db.with do |client| - unless @prepared_statements.include?('truncate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_authors') client.prepare('truncate_authors', TruncateAuthorsSql) - @prepared_statements.add('truncate_authors') + prepared.add('truncate_authors') end client.exec_prepared('truncate_authors') end @@ -430,9 +442,10 @@ def truncate_authors def create_book(create_book_args) @db.with do |client| query_params = [create_book_args.name, create_book_args.author_id] - unless @prepared_statements.include?('create_book') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_book') client.prepare('create_book', CreateBookSql) - @prepared_statements.add('create_book') + prepared.add('create_book') end result = client.exec_prepared('create_book', query_params) return result.first['id'] @@ -441,9 +454,10 @@ def create_book(create_book_args) def list_all_authors_books @db.with do |client| - unless @prepared_statements.include?('list_all_authors_books') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_all_authors_books') client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - @prepared_statements.add('list_all_authors_books') + prepared.add('list_all_authors_books') end result = client.exec_prepared('list_all_authors_books').values entities = [] @@ -456,9 +470,10 @@ def list_all_authors_books def get_duplicate_authors @db.with do |client| - unless @prepared_statements.include?('get_duplicate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_duplicate_authors') client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - @prepared_statements.add('get_duplicate_authors') + prepared.add('get_duplicate_authors') end result = client.exec_prepared('get_duplicate_authors').values entities = [] @@ -472,9 +487,10 @@ def get_duplicate_authors def get_authors_by_book_name(get_authors_by_book_name_args) @db.with do |client| query_params = [get_authors_by_book_name_args.name] - unless @prepared_statements.include?('get_authors_by_book_name') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_book_name') client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - @prepared_statements.add('get_authors_by_book_name') + prepared.add('get_authors_by_book_name') end result = client.exec_prepared('get_authors_by_book_name', query_params).values entities = [] @@ -488,9 +504,10 @@ def get_authors_by_book_name(get_authors_by_book_name_args) def create_extended_bio(create_extended_bio_args) @db.with do |client| query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - unless @prepared_statements.include?('create_extended_bio') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_extended_bio') client.prepare('create_extended_bio', CreateExtendedBioSql) - @prepared_statements.add('create_extended_bio') + prepared.add('create_extended_bio') end client.exec_prepared('create_extended_bio', query_params) end @@ -499,9 +516,10 @@ def create_extended_bio(create_extended_bio_args) def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) @db.with do |client| query_params = [get_first_extended_bio_by_type_args.bio_type] - unless @prepared_statements.include?('get_first_extended_bio_by_type') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_first_extended_bio_by_type') client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - @prepared_statements.add('get_first_extended_bio_by_type') + prepared.add('get_first_extended_bio_by_type') end result = client.exec_prepared('get_first_extended_bio_by_type', query_params) row = result.first @@ -513,9 +531,10 @@ def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) def truncate_extended_bios @db.with do |client| - unless @prepared_statements.include?('truncate_extended_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extended_bios') client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - @prepared_statements.add('truncate_extended_bios') + prepared.add('truncate_extended_bios') end client.exec_prepared('truncate_extended_bios') end @@ -533,9 +552,10 @@ def insert_extra_types(insert_extra_types_args) insert_extra_types_args.c_money, insert_extra_types_args.c_not_null ] - unless @prepared_statements.include?('insert_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('insert_extra_types') client.prepare('insert_extra_types', InsertExtraTypesSql) - @prepared_statements.add('insert_extra_types') + prepared.add('insert_extra_types') end client.exec_prepared('insert_extra_types', query_params) end @@ -543,9 +563,10 @@ def insert_extra_types(insert_extra_types_args) def get_extra_types @db.with do |client| - unless @prepared_statements.include?('get_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_extra_types') client.prepare('get_extra_types', GetExtraTypesSql) - @prepared_statements.add('get_extra_types') + prepared.add('get_extra_types') end result = client.exec_prepared('get_extra_types') row = result.first @@ -566,9 +587,10 @@ def get_extra_types def truncate_extra_types @db.with do |client| - unless @prepared_statements.include?('truncate_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extra_types') client.prepare('truncate_extra_types', TruncateExtraTypesSql) - @prepared_statements.add('truncate_extra_types') + prepared.add('truncate_extra_types') end client.exec_prepared('truncate_extra_types') end diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index b56fa80..05c5292 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -293,7 +293,7 @@ module PgCodegen class QuerySql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index 69fb676..99d3be2 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -19,12 +19,12 @@ def init_code 'client' ].join("\n") pool = "ConnectionPool.new(**connection_pool_params) {\n#{Code.indent(pool_body)}\n}" - body = ["@db = #{pool}", '@prepared_statements = Set[]'].join("\n") + body = ["@db = #{pool}", '@prepared_statements = {}.compare_by_identity'].join("\n") Code.block('def initialize(connection_pool_params, pg_params)', body) end def init_type - "@db: untyped\n@prepared_statements: Set[String]\n" \ + "@db: untyped\n@prepared_statements: Hash[untyped, Set[String]]\n" \ 'def initialize: (Hash[String, String], Hash[String, String]) -> void' end @@ -33,9 +33,11 @@ def transaction_code Code.block('def transaction', Code.block('@db.with do |client|', Code.block('client.transaction do', 'yield self'))) end + # Statements are prepared once per *connection*: each pooled connection has its own server-side statements. def prepare_stmt(func_name, const) - body = "client.prepare('#{func_name}', #{const})\n@prepared_statements.add('#{func_name}')" - Code.block("unless @prepared_statements.include?('#{func_name}')", body) + body = "client.prepare('#{func_name}', #{const})\nprepared.add('#{func_name}')" + "prepared = (@prepared_statements[client] ||= Set[])\n" \ + "#{Code.block("unless prepared.include?('#{func_name}')", body)}" end def execute_stmt(func_name, has_params, array_rows: false) diff --git a/steep_expectations.yml b/steep_expectations.yml index 4e2dbab..5cd7015 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,53 +3,93 @@ diagnostics: - range: start: - line: 98 + line: 192 character: 9 end: - line: 98 + line: 192 character: 23 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 99 + line: 193 character: 4 end: - line: 99 + line: 193 character: 10 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant + - range: + start: + line: 565 + character: 3 + end: + line: 565 + character: 6 + severity: WARNING + message: 'Cannot find the declaration of constant: `CSV`' + code: Ruby::UnknownConstant - file: examples/pg/query_sql.rb diagnostics: - range: start: - line: 68 + line: 213 character: 9 end: - line: 68 + line: 213 character: 23 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 69 + line: 214 character: 13 end: - line: 69 + line: 214 character: 15 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 70 + line: 215 + character: 15 + end: + line: 215 + character: 17 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 216 + character: 32 + end: + line: 216 + character: 34 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 218 character: 34 end: - line: 70 + line: 218 character: 36 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant + - range: + start: + line: 422 + character: 14 + end: + line: 422 + character: 16 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant diff --git a/test/golden/pg/default/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb index 88c372a..5cb8646 100644 --- a/test/golden/pg/default/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -21,7 +21,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -35,9 +35,10 @@ def transaction def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] - unless @prepared_statements.include?('with_numbers2_go') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('with_numbers2_go') client.prepare('with_numbers2_go', WithNumbers2GoSql) - @prepared_statements.add('with_numbers2_go') + prepared.add('with_numbers2_go') end client.exec_prepared('with_numbers2_go', query_params) end diff --git a/test/golden/pg/default/my_queries_sql.rbs b/test/golden/pg/default/my_queries_sql.rbs index d245c44..2cfe350 100644 --- a/test/golden/pg/default/my_queries_sql.rbs +++ b/test/golden/pg/default/my_queries_sql.rbs @@ -9,7 +9,7 @@ module PgCodegen class My_queriesSql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/default/other_sql.rb b/test/golden/pg/default/other_sql.rb index a81600c..59b7056 100644 --- a/test/golden/pg/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -39,7 +39,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -59,9 +59,10 @@ def no_cols(no_cols_args) no_cols_args.c3, no_cols_args.c4 ] - unless @prepared_statements.include?('no_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('no_cols') client.prepare('no_cols', NoColsSql) - @prepared_statements.add('no_cols') + prepared.add('no_cols') end client.exec_prepared('no_cols', query_params) end @@ -69,9 +70,10 @@ def no_cols(no_cols_args) def other_one @db.with do |client| - unless @prepared_statements.include?('other_one') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('other_one') client.prepare('other_one', OtherOneSql) - @prepared_statements.add('other_one') + prepared.add('other_one') end result = client.exec_prepared('other_one') row = result.first @@ -84,9 +86,10 @@ def other_one def count_rows(count_rows_args) @db.with do |client| query_params = [count_rows_args.c0] - unless @prepared_statements.include?('count_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('count_rows') client.prepare('count_rows', CountRowsSql) - @prepared_statements.add('count_rows') + prepared.add('count_rows') end result = client.exec_prepared('count_rows', query_params) return result.cmd_tuples diff --git a/test/golden/pg/default/other_sql.rbs b/test/golden/pg/default/other_sql.rbs index e287a6c..264bd6f 100644 --- a/test/golden/pg/default/other_sql.rbs +++ b/test/golden/pg/default/other_sql.rbs @@ -33,7 +33,7 @@ module PgCodegen class OtherSql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb index f832ff3..60af7b6 100644 --- a/test/golden/pg/default/query_sql.rb +++ b/test/golden/pg/default/query_sql.rb @@ -254,7 +254,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -268,9 +268,10 @@ def transaction def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author') client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') + prepared.add('get_author') end result = client.exec_prepared('get_author', query_params) row = result.first @@ -282,9 +283,10 @@ def get_author(get_author_args) def list_authors @db.with do |client| - unless @prepared_statements.include?('list_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors') client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') + prepared.add('list_authors') end result = client.exec_prepared('list_authors') entities = [] @@ -298,9 +300,10 @@ def list_authors def create_author(create_author_args) @db.with do |client| query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author') client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') + prepared.add('create_author') end result = client.exec_prepared('create_author', query_params) row = result.first @@ -313,9 +316,10 @@ def create_author(create_author_args) def delete_author(delete_author_args) @db.with do |client| query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('delete_author') client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') + prepared.add('delete_author') end client.exec_prepared('delete_author', query_params) end @@ -323,9 +327,10 @@ def delete_author(delete_author_args) def test @db.with do |client| - unless @prepared_statements.include?('test') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('test') client.prepare('test', TestSql) - @prepared_statements.add('test') + prepared.add('test') end result = client.exec_prepared('test') row = result.first @@ -357,9 +362,10 @@ def test def list_authors_paged(list_authors_paged_args) @db.with do |client| query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - unless @prepared_statements.include?('list_authors_paged') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors_paged') client.prepare('list_authors_paged', ListAuthorsPagedSql) - @prepared_statements.add('list_authors_paged') + prepared.add('list_authors_paged') end result = client.exec_prepared('list_authors_paged', query_params) entities = [] @@ -373,9 +379,10 @@ def list_authors_paged(list_authors_paged_args) def get_author_by_name_pattern(get_author_by_name_pattern_args) @db.with do |client| query_params = [get_author_by_name_pattern_args.name_pattern] - unless @prepared_statements.include?('get_author_by_name_pattern') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author_by_name_pattern') client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - @prepared_statements.add('get_author_by_name_pattern') + prepared.add('get_author_by_name_pattern') end result = client.exec_prepared('get_author_by_name_pattern', query_params) entities = [] @@ -389,9 +396,10 @@ def get_author_by_name_pattern(get_author_by_name_pattern_args) def create_author_return_id(create_author_return_id_args) @db.with do |client| query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - unless @prepared_statements.include?('create_author_return_id') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author_return_id') client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - @prepared_statements.add('create_author_return_id') + prepared.add('create_author_return_id') end result = client.exec_prepared('create_author_return_id', query_params) return result.first['id'] @@ -401,9 +409,10 @@ def create_author_return_id(create_author_return_id_args) def update_author_bios(update_author_bios_args) @db.with do |client| query_params = [update_author_bios_args.bio] - unless @prepared_statements.include?('update_author_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('update_author_bios') client.prepare('update_author_bios', UpdateAuthorBiosSql) - @prepared_statements.add('update_author_bios') + prepared.add('update_author_bios') end result = client.exec_prepared('update_author_bios', query_params) return result.cmd_tuples @@ -413,9 +422,10 @@ def update_author_bios(update_author_bios_args) def get_authors_by_ids(get_authors_by_ids_args) @db.with do |client| query_params = [get_authors_by_ids_args.param1] - unless @prepared_statements.include?('get_authors_by_ids') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids') client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - @prepared_statements.add('get_authors_by_ids') + prepared.add('get_authors_by_ids') end result = client.exec_prepared('get_authors_by_ids', query_params) entities = [] @@ -429,9 +439,10 @@ def get_authors_by_ids(get_authors_by_ids_args) def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) @db.with do |client| query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - unless @prepared_statements.include?('get_authors_by_ids_and_names') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids_and_names') client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - @prepared_statements.add('get_authors_by_ids_and_names') + prepared.add('get_authors_by_ids_and_names') end result = client.exec_prepared('get_authors_by_ids_and_names', query_params) entities = [] @@ -455,9 +466,10 @@ def copy_authors(copy_authors_args) def truncate_authors @db.with do |client| - unless @prepared_statements.include?('truncate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_authors') client.prepare('truncate_authors', TruncateAuthorsSql) - @prepared_statements.add('truncate_authors') + prepared.add('truncate_authors') end client.exec_prepared('truncate_authors') end @@ -466,9 +478,10 @@ def truncate_authors def create_book(create_book_args) @db.with do |client| query_params = [create_book_args.name, create_book_args.author_id] - unless @prepared_statements.include?('create_book') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_book') client.prepare('create_book', CreateBookSql) - @prepared_statements.add('create_book') + prepared.add('create_book') end result = client.exec_prepared('create_book', query_params) return result.first['id'] @@ -477,9 +490,10 @@ def create_book(create_book_args) def list_all_authors_books @db.with do |client| - unless @prepared_statements.include?('list_all_authors_books') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_all_authors_books') client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - @prepared_statements.add('list_all_authors_books') + prepared.add('list_all_authors_books') end result = client.exec_prepared('list_all_authors_books').values entities = [] @@ -492,9 +506,10 @@ def list_all_authors_books def get_duplicate_authors @db.with do |client| - unless @prepared_statements.include?('get_duplicate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_duplicate_authors') client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - @prepared_statements.add('get_duplicate_authors') + prepared.add('get_duplicate_authors') end result = client.exec_prepared('get_duplicate_authors').values entities = [] @@ -508,9 +523,10 @@ def get_duplicate_authors def get_authors_by_book_name(get_authors_by_book_name_args) @db.with do |client| query_params = [get_authors_by_book_name_args.name] - unless @prepared_statements.include?('get_authors_by_book_name') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_book_name') client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - @prepared_statements.add('get_authors_by_book_name') + prepared.add('get_authors_by_book_name') end result = client.exec_prepared('get_authors_by_book_name', query_params).values entities = [] @@ -524,9 +540,10 @@ def get_authors_by_book_name(get_authors_by_book_name_args) def create_extended_bio(create_extended_bio_args) @db.with do |client| query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - unless @prepared_statements.include?('create_extended_bio') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_extended_bio') client.prepare('create_extended_bio', CreateExtendedBioSql) - @prepared_statements.add('create_extended_bio') + prepared.add('create_extended_bio') end client.exec_prepared('create_extended_bio', query_params) end @@ -535,9 +552,10 @@ def create_extended_bio(create_extended_bio_args) def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) @db.with do |client| query_params = [get_first_extended_bio_by_type_args.bio_type] - unless @prepared_statements.include?('get_first_extended_bio_by_type') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_first_extended_bio_by_type') client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - @prepared_statements.add('get_first_extended_bio_by_type') + prepared.add('get_first_extended_bio_by_type') end result = client.exec_prepared('get_first_extended_bio_by_type', query_params) row = result.first @@ -549,9 +567,10 @@ def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) def truncate_extended_bios @db.with do |client| - unless @prepared_statements.include?('truncate_extended_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extended_bios') client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - @prepared_statements.add('truncate_extended_bios') + prepared.add('truncate_extended_bios') end client.exec_prepared('truncate_extended_bios') end @@ -569,9 +588,10 @@ def insert_extra_types(insert_extra_types_args) insert_extra_types_args.c_money, insert_extra_types_args.c_not_null ] - unless @prepared_statements.include?('insert_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('insert_extra_types') client.prepare('insert_extra_types', InsertExtraTypesSql) - @prepared_statements.add('insert_extra_types') + prepared.add('insert_extra_types') end client.exec_prepared('insert_extra_types', query_params) end @@ -579,9 +599,10 @@ def insert_extra_types(insert_extra_types_args) def get_extra_types @db.with do |client| - unless @prepared_statements.include?('get_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_extra_types') client.prepare('get_extra_types', GetExtraTypesSql) - @prepared_statements.add('get_extra_types') + prepared.add('get_extra_types') end result = client.exec_prepared('get_extra_types') row = result.first @@ -602,9 +623,10 @@ def get_extra_types def truncate_extra_types @db.with do |client| - unless @prepared_statements.include?('truncate_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extra_types') client.prepare('truncate_extra_types', TruncateExtraTypesSql) - @prepared_statements.add('truncate_extra_types') + prepared.add('truncate_extra_types') end client.exec_prepared('truncate_extra_types') end @@ -619,9 +641,10 @@ def many_params(many_params_args) many_params_args.c3, many_params_args.c4 ] - unless @prepared_statements.include?('many_params') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_params') client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') + prepared.add('many_params') end result = client.exec_prepared('many_params', query_params) row = result.first @@ -634,9 +657,10 @@ def many_params(many_params_args) def many_rows(many_rows_args) @db.with do |client| query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_rows') client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') + prepared.add('many_rows') end result = client.exec_prepared('many_rows', query_params) entities = [] @@ -655,9 +679,10 @@ def many_rows(many_rows_args) def four_cols @db.with do |client| - unless @prepared_statements.include?('four_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('four_cols') client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') + prepared.add('four_cols') end result = client.exec_prepared('four_cols') entities = [] diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs index 7b5f549..a5f6d04 100644 --- a/test/golden/pg/default/query_sql.rbs +++ b/test/golden/pg/default/query_sql.rbs @@ -348,7 +348,7 @@ module PgCodegen class QuerySql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb index 61b0d85..9b1269e 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -21,7 +21,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -35,9 +35,10 @@ def transaction def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] - unless @prepared_statements.include?('with_numbers2_go') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('with_numbers2_go') client.prepare('with_numbers2_go', WithNumbers2GoSql) - @prepared_statements.add('with_numbers2_go') + prepared.add('with_numbers2_go') end client.exec_prepared('with_numbers2_go', query_params) end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs index 1f667e5..147f94d 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs @@ -9,7 +9,7 @@ module PgCodegen class My_queriesSql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb index 1b58d8a..4520b46 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -39,7 +39,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -59,9 +59,10 @@ def no_cols(no_cols_args) no_cols_args.c3, no_cols_args.c4 ] - unless @prepared_statements.include?('no_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('no_cols') client.prepare('no_cols', NoColsSql) - @prepared_statements.add('no_cols') + prepared.add('no_cols') end client.exec_prepared('no_cols', query_params) end @@ -69,9 +70,10 @@ def no_cols(no_cols_args) def other_one @db.with do |client| - unless @prepared_statements.include?('other_one') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('other_one') client.prepare('other_one', OtherOneSql) - @prepared_statements.add('other_one') + prepared.add('other_one') end result = client.exec_prepared('other_one') row = result.first @@ -84,9 +86,10 @@ def other_one def count_rows(count_rows_args) @db.with do |client| query_params = [count_rows_args.c0] - unless @prepared_statements.include?('count_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('count_rows') client.prepare('count_rows', CountRowsSql) - @prepared_statements.add('count_rows') + prepared.add('count_rows') end result = client.exec_prepared('count_rows', query_params) return result.cmd_tuples diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rbs b/test/golden/pg/ruby31_gemfile/other_sql.rbs index 204d826..3c402bc 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -33,7 +33,7 @@ module PgCodegen class OtherSql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb index 27adf22..adb72a6 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rb +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -254,7 +254,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -268,9 +268,10 @@ def transaction def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author') client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') + prepared.add('get_author') end result = client.exec_prepared('get_author', query_params) row = result.first @@ -282,9 +283,10 @@ def get_author(get_author_args) def list_authors @db.with do |client| - unless @prepared_statements.include?('list_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors') client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') + prepared.add('list_authors') end result = client.exec_prepared('list_authors') entities = [] @@ -298,9 +300,10 @@ def list_authors def create_author(create_author_args) @db.with do |client| query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author') client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') + prepared.add('create_author') end result = client.exec_prepared('create_author', query_params) row = result.first @@ -313,9 +316,10 @@ def create_author(create_author_args) def delete_author(delete_author_args) @db.with do |client| query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('delete_author') client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') + prepared.add('delete_author') end client.exec_prepared('delete_author', query_params) end @@ -323,9 +327,10 @@ def delete_author(delete_author_args) def test @db.with do |client| - unless @prepared_statements.include?('test') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('test') client.prepare('test', TestSql) - @prepared_statements.add('test') + prepared.add('test') end result = client.exec_prepared('test') row = result.first @@ -357,9 +362,10 @@ def test def list_authors_paged(list_authors_paged_args) @db.with do |client| query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - unless @prepared_statements.include?('list_authors_paged') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors_paged') client.prepare('list_authors_paged', ListAuthorsPagedSql) - @prepared_statements.add('list_authors_paged') + prepared.add('list_authors_paged') end result = client.exec_prepared('list_authors_paged', query_params) entities = [] @@ -373,9 +379,10 @@ def list_authors_paged(list_authors_paged_args) def get_author_by_name_pattern(get_author_by_name_pattern_args) @db.with do |client| query_params = [get_author_by_name_pattern_args.name_pattern] - unless @prepared_statements.include?('get_author_by_name_pattern') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author_by_name_pattern') client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - @prepared_statements.add('get_author_by_name_pattern') + prepared.add('get_author_by_name_pattern') end result = client.exec_prepared('get_author_by_name_pattern', query_params) entities = [] @@ -389,9 +396,10 @@ def get_author_by_name_pattern(get_author_by_name_pattern_args) def create_author_return_id(create_author_return_id_args) @db.with do |client| query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - unless @prepared_statements.include?('create_author_return_id') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author_return_id') client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - @prepared_statements.add('create_author_return_id') + prepared.add('create_author_return_id') end result = client.exec_prepared('create_author_return_id', query_params) return result.first['id'] @@ -401,9 +409,10 @@ def create_author_return_id(create_author_return_id_args) def update_author_bios(update_author_bios_args) @db.with do |client| query_params = [update_author_bios_args.bio] - unless @prepared_statements.include?('update_author_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('update_author_bios') client.prepare('update_author_bios', UpdateAuthorBiosSql) - @prepared_statements.add('update_author_bios') + prepared.add('update_author_bios') end result = client.exec_prepared('update_author_bios', query_params) return result.cmd_tuples @@ -413,9 +422,10 @@ def update_author_bios(update_author_bios_args) def get_authors_by_ids(get_authors_by_ids_args) @db.with do |client| query_params = [get_authors_by_ids_args.param1] - unless @prepared_statements.include?('get_authors_by_ids') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids') client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - @prepared_statements.add('get_authors_by_ids') + prepared.add('get_authors_by_ids') end result = client.exec_prepared('get_authors_by_ids', query_params) entities = [] @@ -429,9 +439,10 @@ def get_authors_by_ids(get_authors_by_ids_args) def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) @db.with do |client| query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - unless @prepared_statements.include?('get_authors_by_ids_and_names') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids_and_names') client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - @prepared_statements.add('get_authors_by_ids_and_names') + prepared.add('get_authors_by_ids_and_names') end result = client.exec_prepared('get_authors_by_ids_and_names', query_params) entities = [] @@ -455,9 +466,10 @@ def copy_authors(copy_authors_args) def truncate_authors @db.with do |client| - unless @prepared_statements.include?('truncate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_authors') client.prepare('truncate_authors', TruncateAuthorsSql) - @prepared_statements.add('truncate_authors') + prepared.add('truncate_authors') end client.exec_prepared('truncate_authors') end @@ -466,9 +478,10 @@ def truncate_authors def create_book(create_book_args) @db.with do |client| query_params = [create_book_args.name, create_book_args.author_id] - unless @prepared_statements.include?('create_book') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_book') client.prepare('create_book', CreateBookSql) - @prepared_statements.add('create_book') + prepared.add('create_book') end result = client.exec_prepared('create_book', query_params) return result.first['id'] @@ -477,9 +490,10 @@ def create_book(create_book_args) def list_all_authors_books @db.with do |client| - unless @prepared_statements.include?('list_all_authors_books') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_all_authors_books') client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - @prepared_statements.add('list_all_authors_books') + prepared.add('list_all_authors_books') end result = client.exec_prepared('list_all_authors_books').values entities = [] @@ -492,9 +506,10 @@ def list_all_authors_books def get_duplicate_authors @db.with do |client| - unless @prepared_statements.include?('get_duplicate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_duplicate_authors') client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - @prepared_statements.add('get_duplicate_authors') + prepared.add('get_duplicate_authors') end result = client.exec_prepared('get_duplicate_authors').values entities = [] @@ -508,9 +523,10 @@ def get_duplicate_authors def get_authors_by_book_name(get_authors_by_book_name_args) @db.with do |client| query_params = [get_authors_by_book_name_args.name] - unless @prepared_statements.include?('get_authors_by_book_name') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_book_name') client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - @prepared_statements.add('get_authors_by_book_name') + prepared.add('get_authors_by_book_name') end result = client.exec_prepared('get_authors_by_book_name', query_params).values entities = [] @@ -524,9 +540,10 @@ def get_authors_by_book_name(get_authors_by_book_name_args) def create_extended_bio(create_extended_bio_args) @db.with do |client| query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - unless @prepared_statements.include?('create_extended_bio') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_extended_bio') client.prepare('create_extended_bio', CreateExtendedBioSql) - @prepared_statements.add('create_extended_bio') + prepared.add('create_extended_bio') end client.exec_prepared('create_extended_bio', query_params) end @@ -535,9 +552,10 @@ def create_extended_bio(create_extended_bio_args) def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) @db.with do |client| query_params = [get_first_extended_bio_by_type_args.bio_type] - unless @prepared_statements.include?('get_first_extended_bio_by_type') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_first_extended_bio_by_type') client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - @prepared_statements.add('get_first_extended_bio_by_type') + prepared.add('get_first_extended_bio_by_type') end result = client.exec_prepared('get_first_extended_bio_by_type', query_params) row = result.first @@ -549,9 +567,10 @@ def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) def truncate_extended_bios @db.with do |client| - unless @prepared_statements.include?('truncate_extended_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extended_bios') client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - @prepared_statements.add('truncate_extended_bios') + prepared.add('truncate_extended_bios') end client.exec_prepared('truncate_extended_bios') end @@ -569,9 +588,10 @@ def insert_extra_types(insert_extra_types_args) insert_extra_types_args.c_money, insert_extra_types_args.c_not_null ] - unless @prepared_statements.include?('insert_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('insert_extra_types') client.prepare('insert_extra_types', InsertExtraTypesSql) - @prepared_statements.add('insert_extra_types') + prepared.add('insert_extra_types') end client.exec_prepared('insert_extra_types', query_params) end @@ -579,9 +599,10 @@ def insert_extra_types(insert_extra_types_args) def get_extra_types @db.with do |client| - unless @prepared_statements.include?('get_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_extra_types') client.prepare('get_extra_types', GetExtraTypesSql) - @prepared_statements.add('get_extra_types') + prepared.add('get_extra_types') end result = client.exec_prepared('get_extra_types') row = result.first @@ -602,9 +623,10 @@ def get_extra_types def truncate_extra_types @db.with do |client| - unless @prepared_statements.include?('truncate_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extra_types') client.prepare('truncate_extra_types', TruncateExtraTypesSql) - @prepared_statements.add('truncate_extra_types') + prepared.add('truncate_extra_types') end client.exec_prepared('truncate_extra_types') end @@ -619,9 +641,10 @@ def many_params(many_params_args) many_params_args.c3, many_params_args.c4 ] - unless @prepared_statements.include?('many_params') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_params') client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') + prepared.add('many_params') end result = client.exec_prepared('many_params', query_params) row = result.first @@ -634,9 +657,10 @@ def many_params(many_params_args) def many_rows(many_rows_args) @db.with do |client| query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_rows') client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') + prepared.add('many_rows') end result = client.exec_prepared('many_rows', query_params) entities = [] @@ -655,9 +679,10 @@ def many_rows(many_rows_args) def four_cols @db.with do |client| - unless @prepared_statements.include?('four_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('four_cols') client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') + prepared.add('four_cols') end result = client.exec_prepared('four_cols') entities = [] diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs index bd5c2b4..da08155 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -348,7 +348,7 @@ module PgCodegen class QuerySql @db: untyped - @prepared_statements: Set[String] + @prepared_statements: Hash[untyped, Set[String]] def initialize: (Hash[String, String], Hash[String, String]) -> void def transaction: [T] () { (self) -> T } -> T diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb index 88c372a..5cb8646 100644 --- a/test/golden/pg/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -21,7 +21,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -35,9 +35,10 @@ def transaction def with_numbers2_go(with_numbers2_go_args) @db.with do |client| query_params = [with_numbers2_go_args.c0] - unless @prepared_statements.include?('with_numbers2_go') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('with_numbers2_go') client.prepare('with_numbers2_go', WithNumbers2GoSql) - @prepared_statements.add('with_numbers2_go') + prepared.add('with_numbers2_go') end client.exec_prepared('with_numbers2_go', query_params) end diff --git a/test/golden/pg/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb index a81600c..59b7056 100644 --- a/test/golden/pg/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -39,7 +39,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -59,9 +59,10 @@ def no_cols(no_cols_args) no_cols_args.c3, no_cols_args.c4 ] - unless @prepared_statements.include?('no_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('no_cols') client.prepare('no_cols', NoColsSql) - @prepared_statements.add('no_cols') + prepared.add('no_cols') end client.exec_prepared('no_cols', query_params) end @@ -69,9 +70,10 @@ def no_cols(no_cols_args) def other_one @db.with do |client| - unless @prepared_statements.include?('other_one') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('other_one') client.prepare('other_one', OtherOneSql) - @prepared_statements.add('other_one') + prepared.add('other_one') end result = client.exec_prepared('other_one') row = result.first @@ -84,9 +86,10 @@ def other_one def count_rows(count_rows_args) @db.with do |client| query_params = [count_rows_args.c0] - unless @prepared_statements.include?('count_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('count_rows') client.prepare('count_rows', CountRowsSql) - @prepared_statements.add('count_rows') + prepared.add('count_rows') end result = client.exec_prepared('count_rows', query_params) return result.cmd_tuples diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb index f832ff3..60af7b6 100644 --- a/test/golden/pg/ruby32_notypes/query_sql.rb +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -254,7 +254,7 @@ def initialize(connection_pool_params, pg_params) client.type_map_for_queries = PG::BasicTypeMapForQueries.new client client } - @prepared_statements = Set[] + @prepared_statements = {}.compare_by_identity end def transaction @@ -268,9 +268,10 @@ def transaction def get_author(get_author_args) @db.with do |client| query_params = [get_author_args.id] - unless @prepared_statements.include?('get_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author') client.prepare('get_author', GetAuthorSql) - @prepared_statements.add('get_author') + prepared.add('get_author') end result = client.exec_prepared('get_author', query_params) row = result.first @@ -282,9 +283,10 @@ def get_author(get_author_args) def list_authors @db.with do |client| - unless @prepared_statements.include?('list_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors') client.prepare('list_authors', ListAuthorsSql) - @prepared_statements.add('list_authors') + prepared.add('list_authors') end result = client.exec_prepared('list_authors') entities = [] @@ -298,9 +300,10 @@ def list_authors def create_author(create_author_args) @db.with do |client| query_params = [create_author_args.name, create_author_args.bio] - unless @prepared_statements.include?('create_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author') client.prepare('create_author', CreateAuthorSql) - @prepared_statements.add('create_author') + prepared.add('create_author') end result = client.exec_prepared('create_author', query_params) row = result.first @@ -313,9 +316,10 @@ def create_author(create_author_args) def delete_author(delete_author_args) @db.with do |client| query_params = [delete_author_args.id] - unless @prepared_statements.include?('delete_author') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('delete_author') client.prepare('delete_author', DeleteAuthorSql) - @prepared_statements.add('delete_author') + prepared.add('delete_author') end client.exec_prepared('delete_author', query_params) end @@ -323,9 +327,10 @@ def delete_author(delete_author_args) def test @db.with do |client| - unless @prepared_statements.include?('test') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('test') client.prepare('test', TestSql) - @prepared_statements.add('test') + prepared.add('test') end result = client.exec_prepared('test') row = result.first @@ -357,9 +362,10 @@ def test def list_authors_paged(list_authors_paged_args) @db.with do |client| query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - unless @prepared_statements.include?('list_authors_paged') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_authors_paged') client.prepare('list_authors_paged', ListAuthorsPagedSql) - @prepared_statements.add('list_authors_paged') + prepared.add('list_authors_paged') end result = client.exec_prepared('list_authors_paged', query_params) entities = [] @@ -373,9 +379,10 @@ def list_authors_paged(list_authors_paged_args) def get_author_by_name_pattern(get_author_by_name_pattern_args) @db.with do |client| query_params = [get_author_by_name_pattern_args.name_pattern] - unless @prepared_statements.include?('get_author_by_name_pattern') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_author_by_name_pattern') client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - @prepared_statements.add('get_author_by_name_pattern') + prepared.add('get_author_by_name_pattern') end result = client.exec_prepared('get_author_by_name_pattern', query_params) entities = [] @@ -389,9 +396,10 @@ def get_author_by_name_pattern(get_author_by_name_pattern_args) def create_author_return_id(create_author_return_id_args) @db.with do |client| query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - unless @prepared_statements.include?('create_author_return_id') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_author_return_id') client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - @prepared_statements.add('create_author_return_id') + prepared.add('create_author_return_id') end result = client.exec_prepared('create_author_return_id', query_params) return result.first['id'] @@ -401,9 +409,10 @@ def create_author_return_id(create_author_return_id_args) def update_author_bios(update_author_bios_args) @db.with do |client| query_params = [update_author_bios_args.bio] - unless @prepared_statements.include?('update_author_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('update_author_bios') client.prepare('update_author_bios', UpdateAuthorBiosSql) - @prepared_statements.add('update_author_bios') + prepared.add('update_author_bios') end result = client.exec_prepared('update_author_bios', query_params) return result.cmd_tuples @@ -413,9 +422,10 @@ def update_author_bios(update_author_bios_args) def get_authors_by_ids(get_authors_by_ids_args) @db.with do |client| query_params = [get_authors_by_ids_args.param1] - unless @prepared_statements.include?('get_authors_by_ids') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids') client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - @prepared_statements.add('get_authors_by_ids') + prepared.add('get_authors_by_ids') end result = client.exec_prepared('get_authors_by_ids', query_params) entities = [] @@ -429,9 +439,10 @@ def get_authors_by_ids(get_authors_by_ids_args) def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) @db.with do |client| query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - unless @prepared_statements.include?('get_authors_by_ids_and_names') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_ids_and_names') client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - @prepared_statements.add('get_authors_by_ids_and_names') + prepared.add('get_authors_by_ids_and_names') end result = client.exec_prepared('get_authors_by_ids_and_names', query_params) entities = [] @@ -455,9 +466,10 @@ def copy_authors(copy_authors_args) def truncate_authors @db.with do |client| - unless @prepared_statements.include?('truncate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_authors') client.prepare('truncate_authors', TruncateAuthorsSql) - @prepared_statements.add('truncate_authors') + prepared.add('truncate_authors') end client.exec_prepared('truncate_authors') end @@ -466,9 +478,10 @@ def truncate_authors def create_book(create_book_args) @db.with do |client| query_params = [create_book_args.name, create_book_args.author_id] - unless @prepared_statements.include?('create_book') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_book') client.prepare('create_book', CreateBookSql) - @prepared_statements.add('create_book') + prepared.add('create_book') end result = client.exec_prepared('create_book', query_params) return result.first['id'] @@ -477,9 +490,10 @@ def create_book(create_book_args) def list_all_authors_books @db.with do |client| - unless @prepared_statements.include?('list_all_authors_books') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('list_all_authors_books') client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - @prepared_statements.add('list_all_authors_books') + prepared.add('list_all_authors_books') end result = client.exec_prepared('list_all_authors_books').values entities = [] @@ -492,9 +506,10 @@ def list_all_authors_books def get_duplicate_authors @db.with do |client| - unless @prepared_statements.include?('get_duplicate_authors') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_duplicate_authors') client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - @prepared_statements.add('get_duplicate_authors') + prepared.add('get_duplicate_authors') end result = client.exec_prepared('get_duplicate_authors').values entities = [] @@ -508,9 +523,10 @@ def get_duplicate_authors def get_authors_by_book_name(get_authors_by_book_name_args) @db.with do |client| query_params = [get_authors_by_book_name_args.name] - unless @prepared_statements.include?('get_authors_by_book_name') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_authors_by_book_name') client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - @prepared_statements.add('get_authors_by_book_name') + prepared.add('get_authors_by_book_name') end result = client.exec_prepared('get_authors_by_book_name', query_params).values entities = [] @@ -524,9 +540,10 @@ def get_authors_by_book_name(get_authors_by_book_name_args) def create_extended_bio(create_extended_bio_args) @db.with do |client| query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - unless @prepared_statements.include?('create_extended_bio') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('create_extended_bio') client.prepare('create_extended_bio', CreateExtendedBioSql) - @prepared_statements.add('create_extended_bio') + prepared.add('create_extended_bio') end client.exec_prepared('create_extended_bio', query_params) end @@ -535,9 +552,10 @@ def create_extended_bio(create_extended_bio_args) def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) @db.with do |client| query_params = [get_first_extended_bio_by_type_args.bio_type] - unless @prepared_statements.include?('get_first_extended_bio_by_type') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_first_extended_bio_by_type') client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - @prepared_statements.add('get_first_extended_bio_by_type') + prepared.add('get_first_extended_bio_by_type') end result = client.exec_prepared('get_first_extended_bio_by_type', query_params) row = result.first @@ -549,9 +567,10 @@ def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) def truncate_extended_bios @db.with do |client| - unless @prepared_statements.include?('truncate_extended_bios') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extended_bios') client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - @prepared_statements.add('truncate_extended_bios') + prepared.add('truncate_extended_bios') end client.exec_prepared('truncate_extended_bios') end @@ -569,9 +588,10 @@ def insert_extra_types(insert_extra_types_args) insert_extra_types_args.c_money, insert_extra_types_args.c_not_null ] - unless @prepared_statements.include?('insert_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('insert_extra_types') client.prepare('insert_extra_types', InsertExtraTypesSql) - @prepared_statements.add('insert_extra_types') + prepared.add('insert_extra_types') end client.exec_prepared('insert_extra_types', query_params) end @@ -579,9 +599,10 @@ def insert_extra_types(insert_extra_types_args) def get_extra_types @db.with do |client| - unless @prepared_statements.include?('get_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('get_extra_types') client.prepare('get_extra_types', GetExtraTypesSql) - @prepared_statements.add('get_extra_types') + prepared.add('get_extra_types') end result = client.exec_prepared('get_extra_types') row = result.first @@ -602,9 +623,10 @@ def get_extra_types def truncate_extra_types @db.with do |client| - unless @prepared_statements.include?('truncate_extra_types') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('truncate_extra_types') client.prepare('truncate_extra_types', TruncateExtraTypesSql) - @prepared_statements.add('truncate_extra_types') + prepared.add('truncate_extra_types') end client.exec_prepared('truncate_extra_types') end @@ -619,9 +641,10 @@ def many_params(many_params_args) many_params_args.c3, many_params_args.c4 ] - unless @prepared_statements.include?('many_params') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_params') client.prepare('many_params', ManyParamsSql) - @prepared_statements.add('many_params') + prepared.add('many_params') end result = client.exec_prepared('many_params', query_params) row = result.first @@ -634,9 +657,10 @@ def many_params(many_params_args) def many_rows(many_rows_args) @db.with do |client| query_params = [many_rows_args.c0] - unless @prepared_statements.include?('many_rows') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('many_rows') client.prepare('many_rows', manyRowsSql) - @prepared_statements.add('many_rows') + prepared.add('many_rows') end result = client.exec_prepared('many_rows', query_params) entities = [] @@ -655,9 +679,10 @@ def many_rows(many_rows_args) def four_cols @db.with do |client| - unless @prepared_statements.include?('four_cols') + prepared = (@prepared_statements[client] ||= Set[]) + unless prepared.include?('four_cols') client.prepare('four_cols', FourColsSql) - @prepared_statements.add('four_cols') + prepared.add('four_cols') end result = client.exec_prepared('four_cols') entities = [] diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index ee2e622..1aa66dc 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -12,14 +12,17 @@ class TestPg < Minitest::Test include PgCodegen - def setup - pg_params = { + def pg_params + { 'dbname' => ENV['DB_NAME'], 'host' => ENV['POSTGRES_HOST'], 'port' => ENV['POSTGRES_PORT'], # optional 'user' => ENV['DB_USER'], 'password' => ENV['DB_PASS'] }.compact + end + + def setup @query_sql = QuerySql.new({}, pg_params) @query_sql.truncate_authors # cascades to books @query_sql.truncate_extended_bios @@ -199,8 +202,7 @@ def test_transaction_rolls_back_on_error end def test_transaction_is_isolated_until_commit - other = QuerySql.new({}, { 'dbname' => ENV['DB_NAME'], 'host' => ENV['POSTGRES_HOST'], 'port' => ENV['POSTGRES_PORT'], - 'user' => ENV['DB_USER'], 'password' => ENV['DB_PASS'] }.compact) + other = QuerySql.new({}, pg_params) @query_sql.transaction do |tx| tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'uncommitted', bio: nil)) assert_empty other.list_authors @@ -208,6 +210,31 @@ def test_transaction_is_isolated_until_commit assert_equal ['uncommitted'], other.list_authors.map(&:name) end + # ---- connection pool ------------------------------------------------------------------------------------------- + + def test_every_pooled_connection_prepares_its_own_statements + pooled = QuerySql.new({ size: 3 }, pg_params) + pool = pooled.instance_variable_get(:@db) + held = Queue.new + gates = Array.new(3) { Queue.new } + # three threads, each holding a different connection until it is released + threads = gates.map do |gate| + Thread.new do + pool.with do |client| + held << client.object_id + gate.pop + pooled.list_authors # reentrant: runs on the connection this thread holds + end + end + end + assert_equal 3, Array.new(3) { held.pop }.uniq.size, 'expected three distinct connections' + # one after the other, so that the first run has finished (and "remembered" the statement) before the next one + threads.zip(gates).each do |thread, gate| + gate << true + assert_equal [], thread.value + end + end + # ---- more data types ------------------------------------------------------------------------------------------- def test_extra_types_round_trip @@ -239,8 +266,7 @@ def test_extra_types_nulls end def raw_client - PG.connect(dbname: ENV['DB_NAME'], host: ENV['POSTGRES_HOST'], port: ENV['POSTGRES_PORT'], user: ENV['DB_USER'], - password: ENV['DB_PASS']) + PG.connect(**pg_params.transform_keys(&:to_sym)) end def test_declared_types_match_runtime_values From fd6c7ba6bd066651f3283d04f0ec202e8d710583 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:02:29 +0000 Subject: [PATCH 10/15] Generate idiomatic Ruby: keyword arguments, injectable pool, nested transactions, RuboCop-clean output The generated code no longer mirrors the C# plugin; it is shaped like Ruby data-access code: - query parameters are keyword arguments (`get_author(id: 1)`); named nullable (sqlc.arg/narg) ones default to nil, plain nullable INSERT columns stay explicit; Ruby keywords / camelCase names are mapped (`class_:`) - implicit return values, `result.map`, `row && Row.new(...)`; :exec returns nil - 2-space indentation, no trailing whitespace, `# frozen_string_literal: true`, SQL in frozen raw heredocs named FOO_SQL, query comments become doc comments; output passes RuboCop (rake rubocop, CI step) - `QuerySql.connect(params, **pool_options)` builds a connection_pool, `QuerySql.new(pool)` injects any pool-like object (connections are configured on first use); `moduleName` option for the namespace - transactions: nested calls use SAVEPOINTs, `isolation:` (pg/mysql2) / `mode:` (sqlite3) - PostgreSQL: statements named per query class and tracked per connection in a module mixed into the connection (several query objects may share a pool); `preparedStatements: false` for pgbouncer - shared helpers live in connection.rb/.rbs; Ruby < 3.2 rows are `Struct.new(:id, :name)` (symbols - the generated Struct code was invalid before); `require 'set'` only for Ruby < 3.2 Tests: end2end suites use the new API and add nested transactions, isolation, injected pools, required keywords, and fiber concurrency under the `async` gem (pg and mysql2 queries overlap); unit tests cover the formatting invariants, Struct/Data classes actually being usable, moduleName, unprepared mode. Steep (locked 1.6.0) and RuboCop are clean on the output. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .github/workflows/ci.yml | 3 + .rubocop.yml | 20 + Gemfile | 2 + Gemfile.lock | 32 + README.md | 32 +- Rakefile | 4 + docs/Design.md | 7 + examples/authors/mysql/query.sql | 3 + examples/authors/postgresql/query.sql | 3 + examples/authors/sqlite/query.sql | 3 + examples/mysql2/connection.rb | 17 + examples/mysql2/connection.rbs | 8 + examples/mysql2/models.rb | 130 +- examples/mysql2/models.rbs | 232 +-- examples/mysql2/query_sql.rb | 1143 +++++++------- examples/mysql2/query_sql.rbs | 580 +++---- examples/pg/connection.rb | 19 + examples/pg/connection.rbs | 8 + examples/pg/models.rb | 102 +- examples/pg/models.rbs | 182 +-- examples/pg/query_sql.rb | 1173 +++++++------- examples/pg/query_sql.rbs | 603 ++++---- examples/sqlite3/models.rb | 14 +- examples/sqlite3/models.rbs | 30 +- examples/sqlite3/query_sql.rb | 724 ++++----- examples/sqlite3/query_sql.rbs | 360 ++--- lib/sqlc_gen_ruby/code.rb | 26 +- lib/sqlc_gen_ruby/drivers/base.rb | 49 +- lib/sqlc_gen_ruby/drivers/mysql2.rb | 149 +- lib/sqlc_gen_ruby/drivers/pg.rb | 144 +- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 49 +- lib/sqlc_gen_ruby/generator.rb | 92 +- lib/sqlc_gen_ruby/method_gen.rb | 120 +- lib/sqlc_gen_ruby/options.rb | 14 +- lib/sqlc_gen_ruby/type_resolver.rb | 4 + steep_expectations.yml | 78 +- test/codec_test.rb | 4 +- test/features_test.rb | 129 +- test/fixtures/mysql2.bin | 12 +- test/fixtures/pg.bin | Bin 10951 -> 11146 bytes test/fixtures/sqlite3.bin | Bin 4576 -> 4771 bytes test/golden/mysql2/default/connection.rb | 17 + test/golden/mysql2/default/connection.rbs | 8 + test/golden/mysql2/default/models.rb | 130 +- test/golden/mysql2/default/models.rbs | 232 +-- test/golden/mysql2/default/my_queries_sql.rb | 108 +- test/golden/mysql2/default/my_queries_sql.rbs | 32 +- test/golden/mysql2/default/other_sql.rb | 205 +-- test/golden/mysql2/default/other_sql.rbs | 73 +- test/golden/mysql2/default/query_sql.rb | 1325 ++++++++-------- test/golden/mysql2/default/query_sql.rbs | 682 ++++----- .../mysql2/ruby31_gemfile/connection.rb | 17 + .../mysql2/ruby31_gemfile/connection.rbs | 8 + test/golden/mysql2/ruby31_gemfile/models.rb | 130 +- test/golden/mysql2/ruby31_gemfile/models.rbs | 232 +-- .../mysql2/ruby31_gemfile/my_queries_sql.rb | 108 +- .../mysql2/ruby31_gemfile/my_queries_sql.rbs | 32 +- .../golden/mysql2/ruby31_gemfile/other_sql.rb | 205 +-- .../mysql2/ruby31_gemfile/other_sql.rbs | 73 +- .../golden/mysql2/ruby31_gemfile/query_sql.rb | 1325 ++++++++-------- .../mysql2/ruby31_gemfile/query_sql.rbs | 682 ++++----- .../mysql2/ruby32_notypes/connection.rb | 17 + test/golden/mysql2/ruby32_notypes/models.rb | 130 +- .../mysql2/ruby32_notypes/my_queries_sql.rb | 108 +- .../golden/mysql2/ruby32_notypes/other_sql.rb | 205 +-- .../golden/mysql2/ruby32_notypes/query_sql.rb | 1325 ++++++++-------- test/golden/pg/default/connection.rb | 19 + test/golden/pg/default/connection.rbs | 8 + test/golden/pg/default/models.rb | 102 +- test/golden/pg/default/models.rbs | 182 +-- test/golden/pg/default/my_queries_sql.rb | 113 +- test/golden/pg/default/my_queries_sql.rbs | 35 +- test/golden/pg/default/other_sql.rb | 208 +-- test/golden/pg/default/other_sql.rbs | 76 +- test/golden/pg/default/query_sql.rb | 1352 ++++++++--------- test/golden/pg/default/query_sql.rbs | 705 ++++----- test/golden/pg/ruby31_gemfile/Gemfile | 3 +- test/golden/pg/ruby31_gemfile/connection.rb | 19 + test/golden/pg/ruby31_gemfile/connection.rbs | 8 + test/golden/pg/ruby31_gemfile/models.rb | 102 +- test/golden/pg/ruby31_gemfile/models.rbs | 182 +-- .../pg/ruby31_gemfile/my_queries_sql.rb | 112 +- .../pg/ruby31_gemfile/my_queries_sql.rbs | 35 +- test/golden/pg/ruby31_gemfile/other_sql.rb | 207 +-- test/golden/pg/ruby31_gemfile/other_sql.rbs | 76 +- test/golden/pg/ruby31_gemfile/query_sql.rb | 1351 ++++++++-------- test/golden/pg/ruby31_gemfile/query_sql.rbs | 705 ++++----- test/golden/pg/ruby32_notypes/connection.rb | 19 + test/golden/pg/ruby32_notypes/models.rb | 102 +- .../pg/ruby32_notypes/my_queries_sql.rb | 113 +- test/golden/pg/ruby32_notypes/other_sql.rb | 208 +-- test/golden/pg/ruby32_notypes/query_sql.rb | 1352 ++++++++--------- test/golden/sqlite3/default/models.rb | 14 +- test/golden/sqlite3/default/models.rbs | 30 +- test/golden/sqlite3/default/my_queries_sql.rb | 71 +- .../golden/sqlite3/default/my_queries_sql.rbs | 26 +- test/golden/sqlite3/default/other_sql.rb | 164 +- test/golden/sqlite3/default/other_sql.rbs | 67 +- test/golden/sqlite3/default/query_sql.rb | 900 ++++++----- test/golden/sqlite3/default/query_sql.rbs | 462 +++--- test/golden/sqlite3/ruby31_gemfile/models.rb | 14 +- test/golden/sqlite3/ruby31_gemfile/models.rbs | 30 +- .../sqlite3/ruby31_gemfile/my_queries_sql.rb | 71 +- .../sqlite3/ruby31_gemfile/my_queries_sql.rbs | 26 +- .../sqlite3/ruby31_gemfile/other_sql.rb | 164 +- .../sqlite3/ruby31_gemfile/other_sql.rbs | 67 +- .../sqlite3/ruby31_gemfile/query_sql.rb | 900 ++++++----- .../sqlite3/ruby31_gemfile/query_sql.rbs | 462 +++--- test/golden/sqlite3/ruby32_notypes/models.rb | 14 +- .../sqlite3/ruby32_notypes/my_queries_sql.rb | 71 +- .../sqlite3/ruby32_notypes/other_sql.rb | 164 +- .../sqlite3/ruby32_notypes/query_sql.rb | 900 ++++++----- test/golden_test.rb | 12 +- tests/end2end_mysql2.rb | 143 +- tests/end2end_pg.rb | 156 +- tests/end2end_sqlite3.rb | 84 +- tests/rbs_types.rb | 2 +- 117 files changed, 13167 insertions(+), 12954 deletions(-) create mode 100644 .rubocop.yml create mode 100644 examples/mysql2/connection.rb create mode 100644 examples/mysql2/connection.rbs create mode 100644 examples/pg/connection.rb create mode 100644 examples/pg/connection.rbs create mode 100644 test/golden/mysql2/default/connection.rb create mode 100644 test/golden/mysql2/default/connection.rbs create mode 100644 test/golden/mysql2/ruby31_gemfile/connection.rb create mode 100644 test/golden/mysql2/ruby31_gemfile/connection.rbs create mode 100644 test/golden/mysql2/ruby32_notypes/connection.rb create mode 100644 test/golden/pg/default/connection.rb create mode 100644 test/golden/pg/default/connection.rbs create mode 100644 test/golden/pg/ruby31_gemfile/connection.rb create mode 100644 test/golden/pg/ruby31_gemfile/connection.rbs create mode 100644 test/golden/pg/ruby32_notypes/connection.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a09d6b0..9bd2332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,9 @@ jobs: run: ./scripts/tests/run_codegen.sh ${SQLC_CI_FILE} \ ${{ matrix.ruby-version }} ${{ matrix.generate-types }} ${{ matrix.generate-gemfile }} + - name: RuboCop (generated code) + run: rake rubocop + - name: Steep check if: ${{ matrix.generate-types == 'true' }} run: rake steep diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..8b4dd4d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,20 @@ +# Checks the code generated into examples/ (the plugin's output must stay RuboCop-clean). +AllCops: + NewCops: disable + SuggestExtensions: false + TargetRubyVersion: 3.2 + Exclude: + - 'vendor/**/*' + +# generated code is not hand-written: no metrics / docs cops, SQL lines are long +Metrics: + Enabled: false +Style/Documentation: + Enabled: false +Layout/LineLength: + Enabled: false +Layout/TrailingWhitespace: + AllowInHeredoc: true # trailing spaces inside the SQL text are kept as written +# query names come from the SQL (GetAuthor -> get_author) +Naming/AccessorMethodName: + Enabled: false diff --git a/Gemfile b/Gemfile index 2d50b4f..e934901 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,8 @@ gem 'sqlite3' gem 'minitest' gem 'rake' gem 'steep' +gem 'rubocop', require: false # the code generated into examples/ must stay RuboCop-clean (rake rubocop) +gem 'async' # end-to-end tests run the generated code under the Fiber scheduler # Building dist/plugin.wasm (scripts/wasm/build.sh): provides `rbwasm pack`. group :wasm_build do diff --git a/Gemfile.lock b/Gemfile.lock index de96e20..df8e044 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,14 +13,26 @@ GEM mutex_m tzinfo (~> 2.0) ast (2.4.2) + async (2.46.0) + console (~> 1.29) + fiber-annotation + io-event (~> 1.21) base64 (0.2.0) bigdecimal (3.1.5) concurrent-ruby (1.2.3) connection_pool (2.4.1) + console (1.38.0) + fiber-annotation + fiber-local (~> 1.1) + json csv (3.2.8) drb (2.2.0) ruby2_keywords ffi (1.16.3) + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.1) fileutils (1.7.2) google-protobuf (4.36.2) bigdecimal @@ -30,8 +42,10 @@ GEM rake (~> 13.3) i18n (1.14.5) concurrent-ruby (~> 1.0) + io-event (1.22.3) json (2.7.1) language_server-protocol (3.17.0.3) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -40,6 +54,7 @@ GEM minitest (5.24.0) mutex_m (0.2.0) mysql2 (0.5.6) + parallel (1.28.0) parser (3.3.1.0) ast (~> 2.4.1) racc @@ -52,6 +67,21 @@ GEM ffi (~> 1.0) rbs (3.4.0) abbrev + regexp_parser (2.13.1) + rubocop (1.74.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.40.0) + parser (>= 3.3.1.0) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) ruby_wasm (2.10.1) logger @@ -88,12 +118,14 @@ PLATFORMS ruby DEPENDENCIES + async connection_pool google-protobuf (~> 4.0) minitest mysql2 pg (= 1.5.2) rake + rubocop ruby_wasm (~> 2.7) sqlite3 steep diff --git a/README.md b/README.md index 5138fc3..b1d9463 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ codec), and it is shipped as a single self-contained WebAssembly module — the | generateGemfile | default: `false`
values: `false`,`true` | Yes | Assists you with the integration of SQLC and Ruby by generating a `Gemfile` with the needed dependencies. | | generateTypes | default: `true`
values: `false`,`true` | Yes | Determines whether to generate type signatures in addition to the code. | | overrides | a list, see [below](#override-option) | Yes | Overrides the generated (RBS) type of specific columns / parameters. | +| moduleName | default: `Codegen` (e.g. `PgCodegen`) | Yes | The Ruby module the generated code lives in, e.g. `MyApp::Db`. | +| preparedStatements | default: `true`
values: `false`,`true` | Yes | `pg` only: `false` sends the SQL with every call (`exec_params`) - needed behind pgbouncer in transaction mode. | ### Override option ```yaml @@ -81,6 +83,30 @@ sql: ### [Generated Code Examples](docs/Examples.md) +## Using the generated code +```ruby +require_relative 'db/query_sql' + +queries = PgCodegen::QuerySql.connect({ dbname: 'app', host: 'localhost' }, size: 5) # a ConnectionPool + +author = queries.create_author(name: 'Seuss', bio: nil) # :one with RETURNING -> a frozen Data row +queries.get_author(id: author.id) # => # or nil +queries.list_authors_paged(limit: 10, offset: 0) # :many -> Array of rows +queries.get_authors_by_ids(ids: [1, 2, 3]) # sqlc.slice / array parameter +queries.copy_authors([PgCodegen::CopyAuthorsArgs.new(name: 'a', bio: nil)]) # :copyfrom + +queries.transaction(isolation: :serializable) do |tx| # COMMIT on return, ROLLBACK on error + id = tx.create_author_return_id(name: 'a', bio: nil) + tx.transaction { |inner| inner.create_book(name: 'b', author_id: id) } # nested -> SAVEPOINT +end +``` +* **Keyword arguments:** every query parameter is a keyword argument (missing / unknown ones raise `ArgumentError`). Parameters named with `sqlc.arg` / `sqlc.narg` that are nullable default to `nil`; a plain nullable column (`INSERT ... VALUES ($1, $2)`) has to be passed explicitly. Names are snake_cased, Ruby keywords get a trailing underscore (`class_:`). +* **Rows** are immutable `Data` objects (`Struct` for `rubyVersion` below 3.2), so they work with pattern matching: `case row in { id:, name: }`. +* **Connections:** `QuerySql.connect(params, **pool_options)` builds a [`connection_pool`](https://github.com/mperham/connection_pool); to share a pool you already have (or to inject a fake in tests) use `QuerySql.new(pool)` - anything that responds to `#with` and yields a connection. For SQLite pass a `SQLite3::Database` to `QuerySql.new(db)`. +* **Async / fibers:** the generated code is plain synchronous Ruby and works with the Fiber scheduler (the [`async`](https://github.com/socketry/async) gem): inside `Sync { ... }` pooled queries on `pg` and `mysql2` overlap instead of blocking (see `tests/end2end_pg.rb`). SQLite calls are synchronous C calls and do not overlap. +* **SQL** is kept in frozen heredoc constants (`GET_AUTHOR_SQL`); query comments in the `.sql` file become doc comments on the method; `.rbs` signatures are generated for everything. +* The generated code is RuboCop-clean (`rake rubocop`) and 2-space indented. + ## Supported features ✅ supported · 🚫 not offered by the database / sqlc @@ -92,7 +118,7 @@ sql: | `:exec` | ✅ | ✅ | ✅ | | | `:execrows` | ✅ | ✅ | ✅ | returns the number of affected rows | | `:execlastid` | ✅ | ✅ | ✅ | PostgreSQL / SQLite need a `RETURNING ` clause (the value of that column is returned); MySQL returns `client.last_id` | -| `:copyfrom` | ✅ | ✅ | ✅ | takes an `Array` of `Args`. PostgreSQL: `COPY ... FROM STDIN`. MySQL: `LOAD DATA LOCAL INFILE` from a temporary CSV - needs `local_infile: true` in the `Mysql2::Client` params and `local_infile=1` on the server. SQLite: batched multi-row `INSERT` | +| `:copyfrom` | ✅ | ✅ | ✅ | takes an `Array` of `Args` (a generated `Data` class). PostgreSQL: `COPY ... FROM STDIN`. MySQL: `LOAD DATA LOCAL INFILE` from a temporary CSV - needs `local_infile: true` in the `Mysql2::Client` params and `local_infile=1` on the server. SQLite: batched multi-row `INSERT` | ### Macros | Macro | PostgreSQL | MySQL | SQLite | Notes | @@ -104,12 +130,12 @@ sql: ### Other * **Models:** a `models.rb` / `models.rbs` with one `Data` (`Struct` below Ruby 3.2) class per table, and one module with constants per enum (`ExtendedBioType::MEMOIR`, `ExtendedBioType::ALL`). Enum columns are `String`s. -* **Transactions:** `queries.transaction { |tx| tx.create_author(...) }` runs everything in the block on one connection in a transaction (commit when the block returns, rollback when it raises) and returns the block's value. +* **Transactions:** `queries.transaction { |tx| tx.create_author(...) }` runs everything in the block on one connection in a transaction (commit when the block returns, rollback when it raises) and returns the block's value. Nested calls become savepoints; `isolation:` (`pg`, `mysql2`) / `mode:` (`sqlite3`) choose the isolation level / locking mode. * **Nullability:** nullable columns / parameters are typed `T?` in the `.rbs`. * **Arrays (PostgreSQL):** `Array[T]` for columns and parameters. * Types follow the Ruby classes the drivers actually return (e.g. `BigDecimal` for `numeric`/`decimal`, `Date`, `Time`, `IPAddr`; `json`/`jsonb` are parsed, so `untyped`). -Not applicable to the Ruby plugin (they exist in the C# plugin because of its runtime): `withAsyncSuffix`, `withCancellationToken`, `useDapper`, target framework / csproj / namespace options. +Not applicable to the Ruby plugin (they exist in the C# plugin because of its runtime): `withAsyncSuffix` / `withCancellationToken` (Ruby has no async/await: use the Fiber scheduler, see above), `useDapper`, target framework / csproj / namespace options (`moduleName` is the equivalent of the namespace). # Contributing ## Local plugin development diff --git a/Rakefile b/Rakefile index 90e1895..7c84fef 100755 --- a/Rakefile +++ b/Rakefile @@ -12,6 +12,10 @@ Rake::TestTask.new(:end2end_tests) do |t| # called inside Docker t.pattern = 'tests/end2end_*.rb' end +task :rubocop do + sh 'bundle exec rubocop examples' +end + task :steep do sh 'bundle exec steep check --with-expectations' end diff --git a/docs/Design.md b/docs/Design.md index 39a6185..20a9258 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -46,6 +46,13 @@ The previous C# implementation defined the expected behaviour. Its output is kep `test/codec_test.rb` cross-checks the hand-written protobuf codec against Google's official implementation (test-only dependency). +## What the generated code looks like (and why) +Generated code follows what Ruby code usually looks like rather than mirroring the C# plugin: keyword arguments instead of +parameter objects, `Data` rows, implicit returns, `result.map { ... }`, 2-space indentation, a frozen-string-literal +header, SQL in raw heredocs, `connection_pool` for pooling with an injectable pool, nested transactions via savepoints. +Prepared statements are named per query class and remembered per *connection* (a module mixed into each connection), +so several query objects can share a pool. + ## Beyond the C# port The first version of the Ruby plugin reproduced the C# plugin's output exactly (verified against the C# binary, see git history). It has since gained the sqlc features of the sibling `sqlc-gen-csharp` plugin (`:execrows`, `:execlastid` for diff --git a/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index 0107730..7a60060 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -1,4 +1,5 @@ -- name: GetAuthor :one +-- Returns the author with the given id, or nil when there is none. SELECT * FROM authors WHERE id = ? LIMIT 1; @@ -43,6 +44,7 @@ SELECT * FROM authors WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); -- name: UpdateAuthorBios :execrows +-- Sets the bio of every author that has one and returns how many were changed. UPDATE authors SET bio = sqlc.arg('bio') WHERE bio IS NOT NULL; @@ -54,6 +56,7 @@ SELECT * FROM authors WHERE id IN (sqlc.slice('ids')); SELECT * FROM authors WHERE id IN (sqlc.slice('ids')) AND name IN (sqlc.slice('names')); -- name: CopyAuthors :copyfrom +-- Bulk insert: pass an Array of CopyAuthorsArgs. INSERT INTO authors (name, bio) VALUES (?, ?); -- name: TruncateAuthors :exec diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index 27ef314..b6d24be 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -1,4 +1,5 @@ -- name: GetAuthor :one +-- Returns the author with the given id, or nil when there is none. SELECT * FROM authors WHERE id = $1 LIMIT 1; @@ -36,6 +37,7 @@ WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id; -- name: UpdateAuthorBios :execrows +-- Sets the bio of every author that has one and returns how many were changed. UPDATE authors SET bio = $1 WHERE bio IS NOT NULL; @@ -49,6 +51,7 @@ SELECT * FROM authors WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []); -- name: CopyAuthors :copyfrom +-- Bulk insert: pass an Array of CopyAuthorsArgs. INSERT INTO authors (name, bio) VALUES ($1, $2); -- name: TruncateAuthors :exec diff --git a/examples/authors/sqlite/query.sql b/examples/authors/sqlite/query.sql index c33ec38..0c77ede 100644 --- a/examples/authors/sqlite/query.sql +++ b/examples/authors/sqlite/query.sql @@ -1,4 +1,5 @@ -- name: GetAuthor :one +-- Returns the author with the given id, or nil when there is none. SELECT * FROM authors WHERE id = ? LIMIT 1; @@ -30,6 +31,7 @@ WHERE name LIKE COALESCE(sqlc.narg('name_pattern'), '%'); INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id; -- name: UpdateAuthorBios :execrows +-- Sets the bio of every author that has one and returns how many were changed. UPDATE authors SET bio = ? WHERE bio IS NOT NULL; @@ -41,6 +43,7 @@ SELECT * FROM authors WHERE id IN (sqlc.slice('ids')); SELECT * FROM authors WHERE id IN (sqlc.slice('ids')) AND name IN (sqlc.slice('names')); -- name: CopyAuthors :copyfrom +-- Bulk insert: pass an Array of CopyAuthorsArgs. INSERT INTO authors (name, bio) VALUES (?, ?); -- name: TruncateAuthors :exec diff --git a/examples/mysql2/connection.rb b/examples/mysql2/connection.rb new file mode 100644 index 0000000..9c5d70e --- /dev/null +++ b/examples/mysql2/connection.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + # State kept on every Mysql2::Client the first time it is used. + module ConnectionState + attr_accessor :in_transaction + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/examples/mysql2/connection.rbs b/examples/mysql2/connection.rbs new file mode 100644 index 0000000..4af350d --- /dev/null +++ b/examples/mysql2/connection.rbs @@ -0,0 +1,8 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + module ConnectionState + attr_accessor in_transaction: bool + end + + ISOLATION_LEVELS: Hash[Symbol, String] +end diff --git a/examples/mysql2/models.rb b/examples/mysql2/models.rb index 627f01b..3c5c04f 100644 --- a/examples/mysql2/models.rb +++ b/examples/mysql2/models.rb @@ -1,67 +1,69 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Mysql2Codegen - module ExtendedBiosBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - module ExtendedBiosAuthorType - AUTHOR = "Author" - EDITOR = "Editor" - TRANSLATOR = "Translator" - ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze - end - - class Author < Data.define(:id, :name, :bio) - end - - class NodeMysqlType < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end - - class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = 'Author' + EDITOR = 'Editor' + TRANSLATOR = 'Translator' + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodeMysqlType < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) + end +end diff --git a/examples/mysql2/models.rbs b/examples/mysql2/models.rbs index fd0e34d..f1926fc 100644 --- a/examples/mysql2/models.rbs +++ b/examples/mysql2/models.rbs @@ -1,118 +1,118 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - module ExtendedBiosBioType - AUTOBIOGRAPHY: String - BIOGRAPHY: String - MEMOIR: String - ALL: Array[String] - end - - module ExtendedBiosAuthorType - AUTHOR: String - EDITOR: String - TRANSLATOR: String - ALL: Array[String] - end - - class Author - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class NodeMysqlType - attr_reader c_bit: String? - attr_reader c_tinyint: Integer? - attr_reader c_bool: Integer? - attr_reader c_boolean: Integer? - attr_reader c_smallint: Integer? - attr_reader c_mediumint: Integer? - attr_reader c_int: Integer? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer - attr_reader c_decimal: BigDecimal? - attr_reader c_dec: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_fixed: BigDecimal? - attr_reader c_float: Float? - attr_reader c_double: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: Time? - attr_reader c_datetime: Time? - attr_reader c_timestamp: Time? - attr_reader c_year: Integer? - attr_reader c_char: String? - attr_reader c_nchar: String? - attr_reader c_national_char: String? - attr_reader c_varchar: String? - attr_reader c_binary: String? - attr_reader c_varbinary: String? - attr_reader c_tinyblob: String? - attr_reader c_tinytext: String? - attr_reader c_blob: String? - attr_reader c_text: String? - attr_reader c_mediumblob: String? - attr_reader c_mediumtext: String? - attr_reader c_longblob: String? - attr_reader c_longtext: String? - attr_reader c_json: String? - def initialize: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> void - end - - class Book - attr_reader id: Integer - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( Integer id, String name, Integer author_id, String? description) -> void - end - - class ExtendedBio - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + module ExtendedBiosAuthorType + AUTHOR: String + EDITOR: String + TRANSLATOR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodeMysqlType + attr_reader c_bit: String? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? + attr_reader c_tinytext: String? + attr_reader c_blob: String? + attr_reader c_text: String? + attr_reader c_mediumblob: String? + attr_reader c_mediumtext: String? + attr_reader c_longblob: String? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end + + class ExtendedBio + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end +end diff --git a/examples/mysql2/query_sql.rb b/examples/mysql2/query_sql.rb index dc89ba5..44d849b 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -1,572 +1,587 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' require 'csv' require 'tempfile' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Data.define(:bio, :id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ? OFFSET ?) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:limit, :offset) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio - FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - ensure - stmt.close - end - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - ensure - stmt.close - end - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - return if copy_authors_args.empty? - Tempfile.create(['copy_authors', '.csv']) do |file| - copy_authors_args.each do |item| - file.write(csv_line([item.name, item.bio])) - end - file.flush - client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") - end - end - end - - def truncate_authors - @db.with do |client| - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def list_all_authors_books - @db.with do |client| - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def get_duplicate_authors - @db.with do |client| - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params, as: :array) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] - stmt = client.prepare(CreateExtendedBioSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - stmt = client.prepare(GetFirstExtendedBioByTypeSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) - return entity - ensure - stmt.close - end - end - end - - def truncate_extended_bios - @db.with do |client| - stmt = client.prepare(TruncateExtendedBiosSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - private - - def csv_line(values) - fields = values.map do |value| - case value - when nil then '\N' - when true then '1' - when false then '0' - when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') - else value.to_s.gsub('\\') { '\\\\' } - end - end - CSV.generate_line(fields) - end - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + UPDATE_AUTHOR_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE id = ? + SQL + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ? + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) + SQL + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + DELETE FROM extended_bios + SQL + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + ensure + stmt.close + end + end + end + + def list_authors + with_client do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def update_author(bio:, id:) + with_client do |client| + query_params = [bio, id] + stmt = client.prepare(UPDATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def test + with_client do |client| + stmt = client.prepare(TEST_SQL) + begin + result = stmt.execute + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + ensure + stmt.close + end + end + end + + def list_authors_paged(limit:, offset:) + with_client do |client| + query_params = [limit, offset] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio: nil) + with_client do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(ids:) + with_client do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + with_client do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + return if rows.empty? + + Tempfile.create(['copy_authors', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.name, row.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + nil + end + end + + def truncate_authors + with_client do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + with_client do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def get_duplicate_authors + with_client do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params, as: :array) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:, author_type:) + with_client do |client| + query_params = [author_name, name, bio_type, author_type] + stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + ensure + stmt.close + end + end + end + + def truncate_extended_bios + with_client do |client| + stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + + # Formats one row for LOAD DATA ... FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'. + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index 38fb295..82b68e0 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -1,329 +1,255 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorSql: String - - class UpdateAuthorArgs - attr_reader bio: String? - attr_reader id: Integer - def initialize: ( String? bio, Integer id) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: String? - attr_reader c_tinyint: Integer? - attr_reader c_bool: Integer? - attr_reader c_boolean: Integer? - attr_reader c_smallint: Integer? - attr_reader c_mediumint: Integer? - attr_reader c_int: Integer? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer - attr_reader c_decimal: BigDecimal? - attr_reader c_dec: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_fixed: BigDecimal? - attr_reader c_float: Float? - attr_reader c_double: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: Time? - attr_reader c_datetime: Time? - attr_reader c_timestamp: Time? - attr_reader c_year: Integer? - attr_reader c_char: String? - attr_reader c_nchar: String? - attr_reader c_national_char: String? - attr_reader c_varchar: String? - attr_reader c_binary: String? - attr_reader c_varbinary: String? - attr_reader c_tinyblob: String? - attr_reader c_tinytext: String? - attr_reader c_blob: String? - attr_reader c_text: String? - attr_reader c_mediumblob: String? - attr_reader c_mediumtext: String? - attr_reader c_longblob: String? - attr_reader c_longtext: String? - attr_reader c_json: String? - def initialize: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader limit: Integer - attr_reader offset: Integer - def initialize: ( Integer limit, Integer offset) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader ids: Array[Integer] - def initialize: ( Array[Integer] ids) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader ids: Array[Integer] - attr_reader names: Array[String] - def initialize: ( Array[Integer] ids, Array[String] names) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end - - class GetFirstExtendedBioByTypeArgs - attr_reader bio_type: String? - def initialize: ( String? bio_type) -> void - end - - TruncateExtendedBiosSql: String - - class QuerySql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def update_author: (UpdateAuthorArgs) -> void - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - private - def csv_line: (Array[untyped]) -> String - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + UPDATE_AUTHOR_SQL: String + + CREATE_AUTHOR_RETURN_ID_SQL: String + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow + attr_reader c_bit: String? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? + attr_reader c_tinytext: String? + attr_reader c_blob: String? + attr_reader c_text: String? + attr_reader c_mediumblob: String? + attr_reader c_mediumtext: String? + attr_reader c_longblob: String? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + class QuerySql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def update_author: (bio: String?, id: Integer) -> void + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (?bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String?, name: String?, bio_type: String?, author_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def csv_line: (Array[untyped] values) -> String + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/examples/pg/connection.rb b/examples/pg/connection.rb new file mode 100644 index 0000000..a6a4492 --- /dev/null +++ b/examples/pg/connection.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module PgCodegen + # State kept on every PG::Connection the first time it is used. + module ConnectionState + def prepared_statements + @prepared_statements ||= Set[] + end + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/examples/pg/connection.rbs b/examples/pg/connection.rbs new file mode 100644 index 0000000..3b51951 --- /dev/null +++ b/examples/pg/connection.rbs @@ -0,0 +1,8 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ConnectionState + def prepared_statements: () -> Set[String] + end + + ISOLATION_LEVELS: Hash[Symbol, String] +end diff --git a/examples/pg/models.rb b/examples/pg/models.rb index 9539cb7..6d0eee8 100644 --- a/examples/pg/models.rb +++ b/examples/pg/models.rb @@ -1,53 +1,55 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - class Author < Data.define(:id, :name, :bio) - end - - class NodePostgresType < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end - - class PostgresExtraType < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - class ExtendedBio < Data.define(:author_name, :name, :bio_type) - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end diff --git a/examples/pg/models.rbs b/examples/pg/models.rbs index cec018c..b97a45b 100644 --- a/examples/pg/models.rbs +++ b/examples/pg/models.rbs @@ -1,93 +1,93 @@ # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY: String - BIOGRAPHY: String - MEMOIR: String - ALL: Array[String] - end - - class Author - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class NodePostgresType - attr_reader c_bit: String? - attr_reader c_smallint: Integer? - attr_reader c_boolean: bool? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer? - attr_reader c_decimal: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_real: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: String? - attr_reader c_timestamp: Time? - attr_reader c_char: String? - attr_reader c_varchar: String? - attr_reader c_bytea: String? - attr_reader c_text: String? - attr_reader c_json: untyped - def initialize: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> void - end - - class Book - attr_reader id: String - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( String id, String name, Integer author_id, String? description) -> void - end - - class PostgresExtraType - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - class ExtendedBio - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodePostgresType + attr_reader c_bit: String? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: String? + attr_reader c_timestamp: Time? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: String? + attr_reader c_text: String? + attr_reader c_json: untyped + def initialize: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> void + end + + class Book + attr_reader id: String + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( String id, String name, Integer author_id, String? description) -> void + end + + class PostgresExtraType + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + class ExtendedBio + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end +end diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index b016b7b..cc23eeb 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -1,599 +1,586 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Data.define(:id, :name, :bio) - end - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT $2 - OFFSET $1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE($1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = $1 - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT [])) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:param1) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors - INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = $1) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) - - InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) - - class InsertExtraTypesArgs < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) - - class GetExtraTypesRow < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author') - client.prepare('get_author', GetAuthorSql) - prepared.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - prepared.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - prepared.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - prepared.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('test') - client.prepare('test', TestSql) - prepared.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors_paged') - client.prepare('list_authors_paged', ListAuthorsPagedSql) - prepared.add('list_authors_paged') - end - result = client.exec_prepared('list_authors_paged', query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author_by_name_pattern') - client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - prepared.add('get_author_by_name_pattern') - end - result = client.exec_prepared('get_author_by_name_pattern', query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author_return_id') - client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - prepared.add('create_author_return_id') - end - result = client.exec_prepared('create_author_return_id', query_params) - return result.first['id'] - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('update_author_bios') - client.prepare('update_author_bios', UpdateAuthorBiosSql) - prepared.add('update_author_bios') - end - result = client.exec_prepared('update_author_bios', query_params) - return result.cmd_tuples - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [get_authors_by_ids_args.param1] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids') - client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - prepared.add('get_authors_by_ids') - end - result = client.exec_prepared('get_authors_by_ids', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids_and_names') - client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - prepared.add('get_authors_by_ids_and_names') - end - result = client.exec_prepared('get_authors_by_ids_and_names', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) - client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do - copy_authors_args.each do |item| - client.put_copy_data([item.name, item.bio]) - end - end - end - end - - def truncate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_authors') - client.prepare('truncate_authors', TruncateAuthorsSql) - prepared.add('truncate_authors') - end - client.exec_prepared('truncate_authors') - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_book') - client.prepare('create_book', CreateBookSql) - prepared.add('create_book') - end - result = client.exec_prepared('create_book', query_params) - return result.first['id'] - end - end - - def list_all_authors_books - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_all_authors_books') - client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - prepared.add('list_all_authors_books') - end - result = client.exec_prepared('list_all_authors_books').values - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def get_duplicate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_duplicate_authors') - client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - prepared.add('get_duplicate_authors') - end - result = client.exec_prepared('get_duplicate_authors').values - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_book_name') - client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - prepared.add('get_authors_by_book_name') - end - result = client.exec_prepared('get_authors_by_book_name', query_params).values - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_extended_bio') - client.prepare('create_extended_bio', CreateExtendedBioSql) - prepared.add('create_extended_bio') - end - client.exec_prepared('create_extended_bio', query_params) - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_first_extended_bio_by_type') - client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - prepared.add('get_first_extended_bio_by_type') - end - result = client.exec_prepared('get_first_extended_bio_by_type', query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) - return entity - end - end - - def truncate_extended_bios - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extended_bios') - client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - prepared.add('truncate_extended_bios') - end - client.exec_prepared('truncate_extended_bios') - end - end - - def insert_extra_types(insert_extra_types_args) - @db.with do |client| - query_params = [ - insert_extra_types_args.c_uuid, - insert_extra_types_args.c_jsonb, - insert_extra_types_args.c_text_array, - insert_extra_types_args.c_integer_array, - insert_extra_types_args.c_timestamptz, - insert_extra_types_args.c_inet, - insert_extra_types_args.c_money, - insert_extra_types_args.c_not_null - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('insert_extra_types') - client.prepare('insert_extra_types', InsertExtraTypesSql) - prepared.add('insert_extra_types') - end - client.exec_prepared('insert_extra_types', query_params) - end - end - - def get_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_extra_types') - client.prepare('get_extra_types', GetExtraTypesSql) - prepared.add('get_extra_types') - end - result = client.exec_prepared('get_extra_types') - row = result.first - return nil if row.nil? - entity = GetExtraTypesRow.new( - row['c_uuid'], - row['c_jsonb'], - row['c_text_array'], - row['c_integer_array'], - row['c_timestamptz'], - row['c_inet'], - row['c_money'], - row['c_not_null'] - ) - return entity - end - end - - def truncate_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extra_types') - client.prepare('truncate_extra_types', TruncateExtraTypesSql) - prepared.add('truncate_extra_types') - end - client.exec_prepared('truncate_extra_types') - end - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio + SQL + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = $1 + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + TRUNCATE TABLE authors CASCADE + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1 + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + TRUNCATE TABLE extended.bios + SQL + + INSERT_EXTRA_TYPES_SQL = <<~'SQL' + INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + SQL + + GET_EXTRA_TYPES_SQL = <<~'SQL' + SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1 + SQL + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TRUNCATE_EXTRA_TYPES_SQL = <<~'SQL' + TRUNCATE TABLE postgres_extra_types + SQL + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_get_author') + client.prepare('query_sql_get_author', GET_AUTHOR_SQL) + client.prepared_statements.add('query_sql_get_author') + end + result = client.exec_prepared('query_sql_get_author', query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def list_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_authors') + client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) + client.prepared_statements.add('query_sql_list_authors') + end + result = client.exec_prepared('query_sql_list_authors') + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author') + client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_create_author') + end + result = client.exec_prepared('query_sql_create_author', query_params) + row = result.first + row && CreateAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_delete_author') + client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_delete_author') + end + client.exec_prepared('query_sql_delete_author', query_params) + nil + end + end + + def test + with_client do |client| + unless client.prepared_statements.include?('query_sql_test') + client.prepare('query_sql_test', TEST_SQL) + client.prepared_statements.add('query_sql_test') + end + result = client.exec_prepared('query_sql_test') + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + end + end + + def list_authors_paged(offset:, limit:) + with_client do |client| + query_params = [offset, limit] + unless client.prepared_statements.include?('query_sql_list_authors_paged') + client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) + client.prepared_statements.add('query_sql_list_authors_paged') + end + result = client.exec_prepared('query_sql_list_authors_paged', query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') + client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) + client.prepared_statements.add('query_sql_get_author_by_name_pattern') + end + result = client.exec_prepared('query_sql_get_author_by_name_pattern', query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author_return_id') + client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) + client.prepared_statements.add('query_sql_create_author_return_id') + end + result = client.exec_prepared('query_sql_create_author_return_id', query_params) + result.first['id'] + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + with_client do |client| + query_params = [bio] + unless client.prepared_statements.include?('query_sql_update_author_bios') + client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) + client.prepared_statements.add('query_sql_update_author_bios') + end + result = client.exec_prepared('query_sql_update_author_bios', query_params) + result.cmd_tuples + end + end + + def get_authors_by_ids(param1:) + with_client do |client| + query_params = [param1] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids') + client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids') + end + result = client.exec_prepared('query_sql_get_authors_by_ids', query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_authors_by_ids_and_names(param1:, param2:) + with_client do |client| + query_params = [param1, param2] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') + client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids_and_names') + end + result = client.exec_prepared('query_sql_get_authors_by_ids_and_names', query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_authors') + client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_truncate_authors') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + unless client.prepared_statements.include?('query_sql_create_book') + client.prepare('query_sql_create_book', CREATE_BOOK_SQL) + client.prepared_statements.add('query_sql_create_book') + end + result = client.exec_prepared('query_sql_create_book', query_params) + result.first['id'] + end + end + + def list_all_authors_books + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_all_authors_books') + client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) + client.prepared_statements.add('query_sql_list_all_authors_books') + end + result = client.exec_prepared('query_sql_list_all_authors_books').values + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def get_duplicate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_duplicate_authors') + client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_get_duplicate_authors') + end + result = client.exec_prepared('query_sql_get_duplicate_authors').values + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') + client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) + client.prepared_statements.add('query_sql_get_authors_by_book_name') + end + result = client.exec_prepared('query_sql_get_authors_by_book_name', query_params).values + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:) + with_client do |client| + query_params = [author_name, name, bio_type] + unless client.prepared_statements.include?('query_sql_create_extended_bio') + client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) + client.prepared_statements.add('query_sql_create_extended_bio') + end + client.exec_prepared('query_sql_create_extended_bio', query_params) + nil + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') + client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + client.prepared_statements.add('query_sql_get_first_extended_bio_by_type') + end + result = client.exec_prepared('query_sql_get_first_extended_bio_by_type', query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + end + end + + def truncate_extended_bios + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extended_bios') + client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) + client.prepared_statements.add('query_sql_truncate_extended_bios') + end + client.exec_prepared('query_sql_truncate_extended_bios') + nil + end + end + + def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) + with_client do |client| + query_params = [ + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ] + unless client.prepared_statements.include?('query_sql_insert_extra_types') + client.prepare('query_sql_insert_extra_types', INSERT_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_insert_extra_types') + end + client.exec_prepared('query_sql_insert_extra_types', query_params) + nil + end + end + + def get_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_extra_types') + client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_get_extra_types') + end + result = client.exec_prepared('query_sql_get_extra_types') + row = result.first + row && GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + end + end + + def truncate_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extra_types') + client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_truncate_extra_types') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index 05c5292..fe3e8a5 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -1,347 +1,260 @@ # auto-generated by sqlc - do not edit module PgCodegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: String? - attr_reader c_smallint: Integer? - attr_reader c_boolean: bool? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer? - attr_reader c_decimal: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_real: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: String? - attr_reader c_timestamp: Time? - attr_reader c_char: String? - attr_reader c_varchar: String? - attr_reader c_bytea: String? - attr_reader c_text: String? - attr_reader c_json: untyped - def initialize: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader offset: Integer - attr_reader limit: Integer - def initialize: ( Integer offset, Integer limit) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader param1: Array[Integer] - def initialize: ( Array[Integer] param1) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader param1: Array[Integer] - attr_reader param2: Array[String] - def initialize: ( Array[Integer] param1, Array[String] param2) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow - attr_reader id: String - def initialize: ( String id) -> void - end - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end - - class GetFirstExtendedBioByTypeArgs - attr_reader bio_type: String? - def initialize: ( String? bio_type) -> void - end - - TruncateExtendedBiosSql: String - - InsertExtraTypesSql: String - - class InsertExtraTypesArgs - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - GetExtraTypesSql: String - - class GetExtraTypesRow - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - TruncateExtraTypesSql: String - - class QuerySql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> CreateAuthorRow? - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> String - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - def insert_extra_types: (InsertExtraTypesArgs) -> void - - def get_extra_types: -> GetExtraTypesRow? - - def truncate_extra_types: -> void - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + class CreateAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow + attr_reader c_bit: String? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: String? + attr_reader c_timestamp: Time? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: String? + attr_reader c_text: String? + attr_reader c_json: untyped + def initialize: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> void + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow + attr_reader id: String + def initialize: ( String id) -> void + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + INSERT_EXTRA_TYPES_SQL: String + + GET_EXTRA_TYPES_SQL: String + + class GetExtraTypesRow + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + TRUNCATE_EXTRA_TYPES_SQL: String + + class QuerySql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> CreateAuthorRow? + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (param1: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (param1: Array[Integer], param2: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String, name: String, bio_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/examples/sqlite3/models.rb b/examples/sqlite3/models.rb index c96b91e..bf6966f 100644 --- a/examples/sqlite3/models.rb +++ b/examples/sqlite3/models.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author < Data.define(:id, :name, :bio) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end -end \ No newline at end of file + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end diff --git a/examples/sqlite3/models.rbs b/examples/sqlite3/models.rbs index df9c5ba..cd9946a 100644 --- a/examples/sqlite3/models.rbs +++ b/examples/sqlite3/models.rbs @@ -1,17 +1,17 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class Book - attr_reader id: Integer - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( Integer id, String name, Integer author_id, String? description) -> void - end -end \ No newline at end of file + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end +end diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index 55eaad0..1e87e12 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -1,368 +1,368 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ?2 OFFSET ?1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - class QuerySql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - ensure - stmt.close - end - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def list_authors_paged(list_authors_paged_args) - client = @db - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - client = @db - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author_return_id(create_author_return_id_args) - client = @db - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def update_author_bios(update_author_bios_args) - client = @db - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - client = @db - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - client = @db - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def copy_authors(copy_authors_args) - client = @db - copy_authors_args.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) - end - end - - def truncate_authors - client = @db - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - - def create_book(create_book_args) - client = @db - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def list_all_authors_books - client = @db - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def get_duplicate_authors - client = @db - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - client = @db - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - private - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + class QuerySql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def delete_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def list_authors_paged(offset:, limit:) + client = @db + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_author_by_name_pattern(name_pattern: nil) + client = @db + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author_return_id(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + client = @db + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(ids:) + client = @db + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(ids:, names:) + client = @db + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + client = @db + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil + end + + def truncate_authors + client = @db + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + + def create_book(name:, author_id:) + client = @db + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + + def get_authors_by_book_name(name:) + client = @db + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index e6ac24f..4ec86e0 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -1,212 +1,152 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader offset: Integer - attr_reader limit: Integer - def initialize: ( Integer offset, Integer limit) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader ids: Array[Integer] - def initialize: ( Array[Integer] ids) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader ids: Array[Integer] - attr_reader names: Array[String] - def initialize: ( Array[Integer] ids, Array[String] names) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - class QuerySql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def delete_author: (DeleteAuthorArgs) -> void - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - private - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + DELETE_AUTHOR_SQL: String + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + class QuerySql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def delete_author: (id: Integer) -> void + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + private + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/lib/sqlc_gen_ruby/code.rb b/lib/sqlc_gen_ruby/code.rb index 0706a36..15a1b01 100644 --- a/lib/sqlc_gen_ruby/code.rb +++ b/lib/sqlc_gen_ruby/code.rb @@ -7,16 +7,11 @@ module Code module_function - def indent(text) - text.split("\n", -1).map { |l| "\t#{l}" }.join("\n") - end + INDENT = ' ' - # Strips from every line as many leading characters as the first line has - # leading whitespace (keeps multi-line values aligned after being indented). - def dedent_to_first_line(text) - lines = text.split("\n", -1) - n = lines.first[/\A\s*/].length - lines.map { |l| l[n..] || '' }.join("\n") + # Indents every non-blank line (blank lines stay empty: no trailing whitespace). + def indent(text) + text.split("\n", -1).map { |l| l.strip.empty? ? '' : "#{INDENT}#{l}" }.join("\n") end def comma_join(items) @@ -58,6 +53,19 @@ def constant_name(value) name end + RUBY_KEYWORDS = %w[BEGIN END alias and begin break case class def defined? do else elsif end ensure false for if in + module next nil not or redo rescue retry return self super then true undef unless until when + while yield __FILE__ __LINE__ __ENCODING__].freeze + + # sqlc column / parameter name -> a valid keyword-argument name (userId -> user_id, class -> class_) + def param_name(name) + snake = snake_case(name) + RUBY_KEYWORDS.include?(snake) ? "#{snake}_" : snake + end + + # GetAuthor => GET_AUTHOR_SQL + def sql_constant(query_name) = "#{snake_case(query_name).upcase}_SQL" + def block(header, body) "#{header}\n#{indent(body)}\nend" end diff --git a/lib/sqlc_gen_ruby/drivers/base.rb b/lib/sqlc_gen_ruby/drivers/base.rb index b4b922b..6784946 100644 --- a/lib/sqlc_gen_ruby/drivers/base.rb +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -22,27 +22,33 @@ def name = raise(NotImplementedError) def required_gems = ['connection_pool', *driver_gems] # Extra `require`s needed by generated code for these queries (stdlib or gems), beyond required_gems. - def extra_requires(_queries) = [] + def extra_requires(_queries, _ruby_version) = [] # Extra gems for the generated Gemfile (e.g. `csv`, a bundled gem since Ruby 3.4). def extra_gems(_queries) = [] - def init_code = raise(NotImplementedError) + # Ruby source (and its RBS) placed in the module next to the query classes: shared helpers / constants. + # [[code, rbs], ...] + def module_members = [] - def init_type = raise(NotImplementedError) + # Public methods of the generated query class that are not queries: constructor, `connect`, `transaction`. + # [[code, rbs], ...] + def class_methods = raise(NotImplementedError) - # `def transaction` of the query class: runs the block with all queries on one connection / transaction. - def transaction_code = raise(NotImplementedError) + # Private helpers of the query class. [[code, rbs], ...] + def private_methods = [] - def transaction_type = "\ndef transaction: [T] () { (self) -> T } -> T" + # How a query method acquires the database connection (yielding `client`). + def client_block = '@db.with do |client|' # RHS of the generated `Sql` constant (without the constant name). def query_text(query) = query.text - def prepare_stmt(_func_name, _const) = raise(NotImplementedError) + # stmt_name: unique per query (e.g. query_sql_get_author), only meaningful for named server-side statements. + def prepare_stmt(_stmt_name, _const) = raise(NotImplementedError) # array_rows: rows are read by position (needed when column names are ambiguous, e.g. sqlc.embed). - def execute_stmt(_func_name, _has_params, array_rows: false) = raise(NotImplementedError) + def execute_stmt(_stmt_name, _const, _has_params, array_rows: false) = raise(NotImplementedError) # Whether generated methods borrow the client from a ConnectionPool (`@db.with do |client|`) # or use the single injected connection (`client = @db`). @@ -62,9 +68,10 @@ def exec_rows_lines(_exec) = raise(NotImplementedError) def exec_last_id_lines(_exec, _query) = raise(NotImplementedError) - def copy_from_lines(_query, _table, _columns) = raise(NotImplementedError) + # table: SQL name; columns: SQL column names; attrs: matching reader names of the rows + def copy_from_lines(_rows_var, _table, _columns, _attrs, _label) = raise(NotImplementedError) - # Source of helper methods the class needs for these queries: [[code, rbs], ...] + # Private helper methods the class needs for these queries: [[code, rbs], ...] def helper_methods(_queries) = [] # Ruby/RBS type of a column's DB type (no nullability / array wrapper). Enums are Strings. @@ -86,11 +93,29 @@ def column_type(column, fallback: :raise) raise ArgumentError, "Unsupported column type: #{column.type.name}" end + # The SQL as a frozen raw heredoc (no escaping issues, readable, no paren-balancing problems). def query_text_constant(query) - const = Code.upcase_first("#{query.name}Sql") - Code.dedent_to_first_line("#{const} = %q(#{query_text(query)})") + const = Code.sql_constant(query.name) + text = query_text(query) + terminator = 'SQL' + terminator += '_' while text.lines.any? { |line| line.strip == terminator } + "#{const} = <<~'#{terminator}'\n#{Code.indent(text)}\n#{terminator}" end + ISOLATION_LEVELS = <<~'RUBY'.chomp + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze + RUBY + + ISOLATION_RBS = 'ISOLATION_LEVELS: Hash[Symbol, String]' + + TRANSACTION_RBS = 'def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | ' \ + ':serializable)?) { (instance) -> T } -> T' + private # from sqlc - char(5), int4, pg_catalog.int8, BLOB, ... diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb index 15d4bae..3bac709 100644 --- a/lib/sqlc_gen_ruby/drivers/mysql2.rb +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -4,36 +4,34 @@ module SqlcGenRuby module Drivers - # Port of Drivers/Mysql2Driver.cs. + # MySQL / MariaDB via the `mysql2` gem (prepared statements, closed after use). class Mysql2 < Base def name = 'Mysql2' - def init_code - pool = "ConnectionPool.new(**connection_pool_params) {\n" \ - "#{Code.indent('Mysql2::Client.new(**mysql2_params)')}\n}" - Code.block('def initialize(connection_pool_params, mysql2_params)', "@db = #{pool}") + def module_members + [ + [CONNECTION_STATE, "module ConnectionState\n attr_accessor in_transaction: bool\nend"], + [ISOLATION_LEVELS, ISOLATION_RBS] + ] end - def init_type - "@db: untyped\n" \ - 'def initialize: (Hash[String, String], Hash[String, String]) -> void' + def class_methods + [ + [CONNECT, 'def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance'], + ["def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [TRANSACTION, TRANSACTION_RBS] + ] end - # mysql2 has no transaction helper: BEGIN / COMMIT / ROLLBACK on the (reentrantly pooled) connection. - def transaction_code - guarded = [ - 'begin', - Code.indent("result = yield self\nclient.query('COMMIT')\nresult"), - 'rescue Exception', - Code.indent("client.query('ROLLBACK')\nraise"), - 'end' - ].join("\n") - Code.block('def transaction', Code.block('@db.with do |client|', "client.query('BEGIN')\n#{guarded}")) + def private_methods + [[WITH_CLIENT, 'def with_client: [T] () { (untyped) -> T } -> T']] end - def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" + def client_block = 'with_client do |client|' - def execute_stmt(_func_name, has_params, array_rows: false) + def prepare_stmt(_stmt_name, const) = "stmt = client.prepare(#{const})" + + def execute_stmt(_stmt_name, _const, has_params, array_rows: false) args = [] args << '*query_params' if has_params args << 'as: :array' if array_rows @@ -44,62 +42,107 @@ def closes_statements? = true def supports_slice? = true - def exec_rows_lines(exec) = [exec, 'return stmt.affected_rows'] + def exec_rows_lines(exec) = [exec, 'stmt.affected_rows'] - def exec_last_id_lines(exec, _query) = [exec, 'return client.last_id'] + def exec_last_id_lines(exec, _query) = [exec, 'client.last_id'] # LOAD DATA LOCAL INFILE from a temporary CSV (the client needs `local_infile: true`, the server # `local_infile=1`). - def copy_from_lines(query, table, columns) - args = Code.snake_case("#{query.name}Args") - write = Code.block("#{args}.each do |item|", - "file.write(csv_line([#{columns.map { |c| "item.#{c}" }.join(', ')}]))") + def copy_from_lines(rows_var, table, columns, attrs, label) + write = Code.block("#{rows_var}.each do |row|", + "file.write(csv_line([#{attrs.map { |a| "row.#{a}" }.join(', ')}]))") load_data = format(LOAD_DATA, table: table, columns: columns.join(', ')) [ - "return if #{args}.empty?", - Code.block("Tempfile.create(['#{Code.snake_case(query.name)}', '.csv']) do |file|", - "#{write}\nfile.flush\n#{load_data}") + "return if #{rows_var}.empty?", + '', + Code.block("Tempfile.create(['#{label}', '.csv']) do |file|", "#{write}\nfile.flush\n#{load_data}"), + 'nil' ] end - def extra_requires(queries) = copy_from?(queries) ? %w[csv tempfile] : [] + def extra_requires(queries, _ruby_version) = copy_from?(queries) ? %w[csv tempfile] : [] def extra_gems(queries) = copy_from?(queries) ? %w[csv] : [] def helper_methods(queries) return [] unless copy_from?(queries) - [[CSV_LINE_CODE, 'def csv_line: (Array[untyped]) -> String']] + [[CSV_LINE, 'def csv_line: (Array[untyped] values) -> String']] end + CONNECTION_STATE = <<~'RUBY'.chomp + # State kept on every Mysql2::Client the first time it is used. + module ConnectionState + attr_accessor :in_transaction + end + RUBY + + CONNECT = <<~'RUBY'.chomp + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + RUBY + + TRANSACTION = <<~'RUBY'.chomp + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + RUBY + + WITH_CLIENT = <<~'RUBY'.chomp + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + RUBY + # Template of the generated statement (single-quoted heredoc: backslashes are copied verbatim). LOAD_DATA = <<~'RUBY'.chomp client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE %
s FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (%s)") RUBY # \N is LOAD DATA's NULL; backslashes are doubled because the statement uses ESCAPED BY '\'. - CSV_LINE_CODE = [ - 'def csv_line(values)', - Code.indent( - [ - 'fields = values.map do |value|', - Code.indent( - [ - 'case value', - "when nil then '\\N'", - "when true then '1'", - "when false then '0'", - "when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N')", - "else value.to_s.gsub('\\\\') { '\\\\\\\\' }", - 'end' - ].join("\n") - ), - 'end', - 'CSV.generate_line(fields)' - ].join("\n") - ), - 'end' - ].join("\n") + CSV_LINE = <<~'RUBY'.chomp + # Formats one row for LOAD DATA ... FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'. + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + RUBY private diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index 99d3be2..7f8235a 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -4,48 +4,61 @@ module SqlcGenRuby module Drivers - # Port of Drivers/PgDriver.cs. Each *_code / *_type pair mirrors the - # BuildCode / BuildType of the C# syntax tree nodes. + # PostgreSQL via the `pg` gem. Prepared statements are named per query class and prepared once per connection. class Pg < Base + def initialize(prepared_statements: true) + super() + @prepared = prepared_statements + end + def name = 'Pg' - def init_code - pool_body = [ - 'client = PG.connect(**pg_params)', - 'type_map = PG::BasicTypeMapForResults.new client', - 'type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings', - 'client.type_map_for_results = type_map', - 'client.type_map_for_queries = PG::BasicTypeMapForQueries.new client', - 'client' - ].join("\n") - pool = "ConnectionPool.new(**connection_pool_params) {\n#{Code.indent(pool_body)}\n}" - body = ["@db = #{pool}", '@prepared_statements = {}.compare_by_identity'].join("\n") - Code.block('def initialize(connection_pool_params, pg_params)', body) + def module_members + [ + [CONNECTION_STATE, "module ConnectionState\n def prepared_statements: () -> Set[String]\nend"], + [ISOLATION_LEVELS, ISOLATION_RBS] + ] end - def init_type - "@db: untyped\n@prepared_statements: Hash[untyped, Set[String]]\n" \ - 'def initialize: (Hash[String, String], Hash[String, String]) -> void' + def class_methods + [ + [CONNECT, 'def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance'], + ["def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [TRANSACTION, TRANSACTION_RBS] + ] end - # PG::Connection#transaction commits when the block returns and rolls back when it raises. - def transaction_code - Code.block('def transaction', Code.block('@db.with do |client|', Code.block('client.transaction do', 'yield self'))) + def private_methods + [ + [WITH_CLIENT, 'def with_client: [T] () { (untyped) -> T } -> T'], + [CONFIGURE, 'def configure: (untyped client) -> void'] + ] end - # Statements are prepared once per *connection*: each pooled connection has its own server-side statements. - def prepare_stmt(func_name, const) - body = "client.prepare('#{func_name}', #{const})\nprepared.add('#{func_name}')" - "prepared = (@prepared_statements[client] ||= Set[])\n" \ - "#{Code.block("unless prepared.include?('#{func_name}')", body)}" + def client_block = 'with_client do |client|' + + # Set is only autoloaded since Ruby 3.2 + def extra_requires(_queries, ruby_version) = ruby_version.immutable_data_supported? ? [] : %w[set] + + def prepare_stmt(stmt_name, const) + return nil unless @prepared + + body = "client.prepare('#{stmt_name}', #{const})\nclient.prepared_statements.add('#{stmt_name}')" + Code.block("unless client.prepared_statements.include?('#{stmt_name}')", body) end - def execute_stmt(func_name, has_params, array_rows: false) - call = "client.exec_prepared('#{func_name}'#{has_params ? ', query_params' : ''})" + # With preparedStatements: false (e.g. behind pgbouncer in transaction mode) the SQL is sent with every call. + def execute_stmt(stmt_name, const, has_params, array_rows: false) + args = has_params ? ', query_params' : '' + call = if @prepared + "client.exec_prepared('#{stmt_name}'#{args})" + else + "client.exec_params(#{const}#{has_params ? ', query_params' : ', []'})" + end array_rows ? "#{call}.values" : call end - def exec_rows_lines(exec) = ["result = #{exec}", 'return result.cmd_tuples'] + def exec_rows_lines(exec) = ["result = #{exec}", 'result.cmd_tuples'] # Relies on a user-written `RETURNING `. def exec_last_id_lines(exec, query) @@ -53,23 +66,84 @@ def exec_last_id_lines(exec, query) raise ArgumentError, "#{query.name}: :execlastid on PostgreSQL needs a RETURNING clause" end - ["result = #{exec}", "return result.first['#{query.columns.first.name}']"] + ["result = #{exec}", "result.first['#{query.columns.first.name}']"] end # COPY ... FROM STDIN, one row at a time through the pg gem's CopyRow encoder. - def copy_from_lines(query, table, columns) - args = Code.snake_case("#{query.name}Args") - put = Code.block("#{args}.each do |item|", - "client.put_copy_data([#{columns.map { |c| "item.#{c}" }.join(', ')}])") + def copy_from_lines(rows_var, table, columns, attrs, _label) + put = Code.block("#{rows_var}.each do |row|", + "client.put_copy_data([#{attrs.map { |a| "row.#{a}" }.join(', ')}])") [ 'encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries)', - Code.block("client.copy_data('COPY #{table} (#{columns.join(', ')}) FROM STDIN', encoder) do", put) + Code.block("client.copy_data('COPY #{table} (#{columns.join(', ')}) FROM STDIN', encoder) do", put), + 'nil' ] end + CONNECTION_STATE = <<~'RUBY'.chomp + # State kept on every PG::Connection the first time it is used. + module ConnectionState + def prepared_statements + @prepared_statements ||= Set[] + end + end + RUBY + + CONNECT = <<~'RUBY'.chomp + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + RUBY + + TRANSACTION = <<~'RUBY'.chomp + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + RUBY + + WITH_CLIENT = <<~'RUBY'.chomp + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + RUBY + + CONFIGURE = <<~'RUBY'.chomp + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + RUBY + private - def driver_gems = %w[pg set] + def driver_gems = %w[pg] def column_mappings # Ruby classes as returned by the pg gem with PG::BasicTypeMapForResults (verified by tests/end2end_pg.rb); diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb index e3c4b13..a6f949f 100644 --- a/lib/sqlc_gen_ruby/drivers/sqlite3.rb +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -4,23 +4,25 @@ module SqlcGenRuby module Drivers - # Port of Drivers/Sqlite3Driver.cs. A single SQLite3::Database is injected, so there is no connection - # pool, and rows are read by index (the sqlite3 gem returns arrays by default). + # SQLite via the `sqlite3` gem. A single SQLite3::Database is injected, so there is no connection pool, and + # rows are read by index (the sqlite3 gem returns arrays by default). class Sqlite3 < Base # SQLITE_MAX_VARIABLE_NUMBER was 999 before SQLite 3.32; stay below it for :copyfrom batches. MAX_BOUND_PARAMETERS = 999 def name = 'Sqlite3' - def init_code = Code.block('def initialize(db)', '@db = db') - - def init_type = "@db: untyped\ndef initialize: (untyped) -> void" - - def transaction_code = Code.block('def transaction', Code.block('@db.transaction do', 'yield self')) + def class_methods + [ + ["# @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads)\n" \ + "def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [TRANSACTION, "def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T"] + ] + end - def prepare_stmt(_func_name, const) = "stmt = client.prepare(#{const})" + def prepare_stmt(_stmt_name, const) = "stmt = client.prepare(#{const})" - def execute_stmt(_func_name, has_params, array_rows: false) + def execute_stmt(_stmt_name, _const, has_params, array_rows: false) has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' end @@ -32,7 +34,7 @@ def closes_statements? = true def supports_slice? = true - def exec_rows_lines(exec) = [exec, 'return client.changes'] + def exec_rows_lines(exec) = [exec, 'client.changes'] # Relies on a user-written `RETURNING `. def exec_last_id_lines(exec, query) @@ -40,21 +42,38 @@ def exec_last_id_lines(exec, query) raise ArgumentError, "#{query.name}: :execlastid on SQLite needs a RETURNING clause" end - ["result = #{exec}", 'row = result.first', 'return row[0]'] + ["result = #{exec}", 'row = result.first', 'row && row[0]'] end # One multi-row `INSERT ... VALUES (?, ?), (?, ?), ...` per batch. - def copy_from_lines(query, table, columns) - args = Code.snake_case("#{query.name}Args") + def copy_from_lines(rows_var, table, columns, attrs, _label) rows_per_batch = [MAX_BOUND_PARAMETERS / columns.size, 1].max placeholders = "(#{(['?'] * columns.size).join(', ')})" body = [ %(sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES \#{Array.new(batch.size, '#{placeholders}').join(', ')}"), - "client.execute(sql, batch.flat_map { |item| [#{columns.map { |c| "item.#{c}" }.join(', ')}] })" + "client.execute(sql, batch.flat_map { |row| [#{attrs.map { |a| "row.#{a}" }.join(', ')}] })" ].join("\n") - [Code.block("#{args}.each_slice(#{rows_per_batch}) do |batch|", body)] + [Code.block("#{rows_var}.each_slice(#{rows_per_batch}) do |batch|", body), 'nil'] end + TRANSACTION = <<~'RUBY'.chomp + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + RUBY + private def driver_gems = %w[sqlite3] diff --git a/lib/sqlc_gen_ruby/generator.rb b/lib/sqlc_gen_ruby/generator.rb index 7913d69..19039c8 100644 --- a/lib/sqlc_gen_ruby/generator.rb +++ b/lib/sqlc_gen_ruby/generator.rb @@ -14,8 +14,10 @@ module SqlcGenRuby # Ruby port of CodeGenerator/CodeGenerator.cs, extended with models, enums, sqlc.embed, ... class Generator - AUTO_GENERATED = '# auto-generated by sqlc - do not edit' + HEADER = "# frozen_string_literal: true\n\n# auto-generated by sqlc - do not edit" + RBS_HEADER = '# auto-generated by sqlc - do not edit' MODELS_FILE = 'models' + CONNECTION_FILE = 'connection' def generate(request) @options = Options.new(request.plugin_options) @@ -24,14 +26,14 @@ def generate(request) @driver.catalog = @catalog @types = TypeResolver.new(@driver, @options) @layout = RowLayout.new(@driver, @catalog, @types) - @method_gen = MethodGen.new(@driver, @layout, @types) name_anonymous_params(request.queries) models = models_files + connection = connection_files files = request.queries.group_by { |q| class_name_for(q.filename) }.flat_map do |class_name, queries| - generate_files(queries, class_name, with_models: !models.empty?) + generate_files(queries, class_name, with_models: !models.empty?, with_connection: !connection.empty?) end - files.concat(models) + files.concat(models).concat(connection) files << gemfile(request.queries) if @options.generate_gemfile Plugin::GenerateResponse.new(files: files) end @@ -50,13 +52,13 @@ def name_anonymous_params(queries) def instantiate_driver case @options.driver_name when 'mysql2' then Drivers::Mysql2.new - when 'pg' then Drivers::Pg.new + when 'pg' then Drivers::Pg.new(prepared_statements: @options.prepared_statements) when 'sqlite3' then Drivers::Sqlite3.new else raise ArgumentError, "unknown driver: #{@options.driver_name}" end end - def module_name = "#{@driver.name}Codegen" + def module_name = @options.module_name || "#{@driver.name}Codegen" # query.sql => QuerySql def class_name_for(filename) @@ -66,60 +68,55 @@ def class_name_for(filename) Code.upcase_first(File.basename(filename, ext)) + Code.upcase_first(ext[1..]) end - def generate_files(queries, class_name, with_models:) - members = queries.flat_map { |q| query_members(q) } - klass = class_members(queries) + def generate_files(queries, class_name, with_models:, with_connection:) + method_gen = MethodGen.new(@driver, @layout, @types, class_name) + members = queries.flat_map { |q| query_members(q, method_gen) } + klass = class_members(queries, method_gen) base = Code.snake_case(class_name) - code_body = (members.map(&:first) + [Code.block("class #{class_name}", klass.map(&:first).join("\n\n"))]) - requires = requires_for(queries, with_models) - code = "#{AUTO_GENERATED}\n#{requires}\n\n#{Code.block("module #{module_name}", code_body.join("\n\n"))}" + code_body = members.map(&:first) + [Code.block("class #{class_name}", klass.map(&:first).join("\n\n"))] + requires = requires_for(queries, with_models, with_connection) + code = "#{HEADER}\n#{requires}\n\n#{Code.block("module #{module_name}", code_body.join("\n\n"))}\n" files = [Plugin::File.new(name: "#{base}.rb", contents: code)] if @options.generate_types - type_body = members.map(&:last).compact + [Code.block("class #{class_name}", klass.map(&:last).join("\n"))] - types = "#{AUTO_GENERATED}\n#{Code.block("module #{module_name}", type_body.join("\n\n"))}" + type_body = members.map(&:last).compact + [Code.block("class #{class_name}", klass.map(&:last).join("\n\n"))] + types = "#{RBS_HEADER}\n#{Code.block("module #{module_name}", type_body.join("\n\n"))}\n" files << Plugin::File.new(name: "#{base}.rbs", contents: types) end files end - def requires_for(queries, with_models) - lines = (@driver.required_gems + @driver.extra_requires(queries)).map { |g| "require '#{g}'" } + def requires_for(queries, with_models, with_connection) + lines = (@driver.required_gems + @driver.extra_requires(queries, @options.ruby_version)).map { |g| "require '#{g}'" } + lines << "require_relative '#{CONNECTION_FILE}'" if with_connection lines << "require_relative '#{MODELS_FILE}'" if with_models lines.join("\n") end - # [[code, rbs_or_nil], ...] for the SQL constant, row class, and args class. - def query_members(query) + # [[code, rbs_or_nil], ...] for the SQL constant, row class, and (for :copyfrom) the class of the copied rows. + def query_members(query, method_gen) members = [] unless query.cmd == ':copyfrom' # copied rows are written by generated code, there is no SQL constant to run - members << [@driver.query_text_constant(query), "#{query.name}Sql: String"] + members << [@driver.query_text_constant(query), "#{Code.sql_constant(query.name)}: String"] end members << dataclass("#{Code.upcase_first(query.name)}Row", row_pairs(query)) unless query.columns.empty? - unless query.params.empty? - members << dataclass("#{Code.upcase_first(query.name)}Args", param_pairs(query)) + if query.cmd == ':copyfrom' + pairs = method_gen.params(query).map { |p| [p.name, p.type] } + members << dataclass("#{Code.upcase_first(query.name)}Args", pairs) end members end def row_pairs(query) = @layout.entries(query).map { |e| [e.attr, e.type] } - def param_pairs(query) - query.params.map do |p| - type = p.column.is_sqlc_slice ? @types.slice_type(p.column, query.name) : @types.type(p.column, query_name: query.name) - [p.column.name, type] - end - end - - def class_members(queries) - members = [[@driver.init_code, @driver.init_type], - [@driver.transaction_code, @driver.transaction_type]] - members.concat(queries.map { |q| [@method_gen.code(q), @method_gen.type(q)] }) - helpers = @method_gen.helper_methods(queries) + # constructor / connect / transaction, one method per query, then the private helpers + def class_members(queries, method_gen) + members = @driver.class_methods.dup + members.concat(queries.map { |q| [method_gen.code(q), method_gen.type(q)] }) + helpers = @driver.private_methods + method_gen.helper_methods(queries) unless helpers.empty? - members << ["private\n\n#{helpers.map(&:first).join("\n\n")}", - "\nprivate\n#{helpers.map(&:last).join("\n")}"] + members << ["private\n\n#{helpers.map(&:first).join("\n\n")}", "private\n\n#{helpers.map(&:last).join("\n\n")}"] end members end @@ -143,7 +140,7 @@ def data_define(name, pairs) end def new_struct(name, pairs) - attrs = Code.comma_join(pairs.map(&:first)) + attrs = Code.comma_join(pairs.map { |n, _| ":#{n}" }) code = "class #{name} < Struct.new(#{attrs})\nend" accessors = Code.indent(pairs.map { |n, t| "attr_accessor #{n}: #{t}" }.join("\n")) init_args = Code.indent(Code.comma_join(pairs.map { |n, t| "#{t} #{n}" })) @@ -163,25 +160,42 @@ def models_files end return [] if members.empty? - code = "#{AUTO_GENERATED}\n\n#{Code.block("module #{module_name}", members.map(&:first).join("\n\n"))}" + code = "#{HEADER}\n\n#{Code.block("module #{module_name}", members.map(&:first).join("\n\n"))}\n" files = [Plugin::File.new(name: "#{MODELS_FILE}.rb", contents: code)] if @options.generate_types - types = "#{AUTO_GENERATED}\n#{Code.block("module #{module_name}", members.map(&:last).join("\n\n"))}" + types = "#{RBS_HEADER}\n#{Code.block("module #{module_name}", members.map(&:last).join("\n\n"))}\n" files << Plugin::File.new(name: "#{MODELS_FILE}.rbs", contents: types) end files end + # connection.rb: helpers shared by the query classes of the driver (per-connection state, constants) + def connection_files + members = @driver.module_members + return [] if members.empty? + + files = [Plugin::File.new(name: "#{CONNECTION_FILE}.rb", + contents: "#{HEADER}\n\n#{Code.block("module #{module_name}", members.map(&:first).join("\n\n"))}\n")] + if @options.generate_types + types = "#{RBS_HEADER}\n#{Code.block("module #{module_name}", members.map(&:last).join("\n\n"))}\n" + files << Plugin::File.new(name: "#{CONNECTION_FILE}.rbs", contents: types) + end + files + end + # module BioType; AUTOBIOGRAPHY = 'Autobiography'; ...; ALL = [...].freeze; end def enum_module(name, values) consts = unique_constants(values) - code = consts.map { |const, value| "#{const} = #{value.inspect}" } + code = consts.map { |const, value| "#{const} = #{quote(value)}" } code << "ALL = [#{consts.map(&:first).join(', ')}].freeze" rbs = consts.map { |const, _| "#{const}: String" } rbs << 'ALL: Array[String]' [Code.block("module #{name}", code.join("\n")), Code.block("module #{name}", rbs.join("\n"))] end + # 'Memoir' (single quotes unless the value needs escaping) + def quote(value) = value.match?(/['\\\n]/) ? value.inspect : "'#{value}'" + def unique_constants(values) seen = Hash.new(0) values.map do |value| diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb index 084765c..7ba1380 100644 --- a/lib/sqlc_gen_ruby/method_gen.rb +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -3,36 +3,59 @@ require_relative 'code' module SqlcGenRuby - # Port of Drivers/MethodGen.cs: builds the code (and RBS signature) of one generated query method, - # parameterised by the driver's pooling / row-access / statement flavour. + # Builds the code (and RBS signature) of one generated query method, parameterised by the driver's + # pooling / row-access / statement flavour. Query parameters become keyword arguments: + # queries.get_author(id: 1) queries.create_author(name: 'x', bio: nil) queries.get_authors_by_ids(ids: [1, 2]) class MethodGen SLICE_HELPER = [ - "def slice_placeholders(values)\n\tvalues.empty? ? 'NULL' : Array.new(values.size, '?').join(', ')\nend", - 'def slice_placeholders: (Array[untyped]) -> String' + "def slice_placeholders(values)\n values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ')\nend", + 'def slice_placeholders: (Array[untyped] values) -> String' ].freeze - # layout: Generator#row_layout; types: TypeResolver - def initialize(driver, layout, types) + # optional: nullable and named by the user (sqlc.arg / sqlc.narg) -> keyword argument defaulting to nil + Param = Struct.new(:name, :column, :slice, :optional, :type) + + # layout: RowLayout, types: TypeResolver, class_name: the generated query class (prefixes statement names) + def initialize(driver, layout, types, class_name) @driver = driver @layout = layout @types = types + @statement_prefix = Code.snake_case(class_name) + end + + def params(query) + query.params.map do |p| + column = p.column + slice = column.is_sqlc_slice + type = slice ? @types.slice_type(column, query.name) : @types.type(column, query_name: query.name) + Param.new(Code.param_name(column.name), column, slice, column.is_named_param && @types.nullable?(column, query.name), type) + end end def code(query) func = Code.snake_case(query.name) - args_var = Code.snake_case("#{query.name}Args") - statements = query.cmd == ':copyfrom' ? copy_statements(query) : query_statements(query, func, args_var) - params = query.params.empty? ? '' : "(#{args_var})" - Code.block("def #{func}#{params}", method_body(statements, query.cmd)) + params = params(query) + if query.cmd == ':copyfrom' + statements = copy_statements(query, params) + signature = '(rows)' + else + statements = query_statements(query, func, params) + signature = params.empty? ? '' : "(#{params.map { |p| p.optional ? "#{p.name}: nil" : "#{p.name}:" }.join(', ')})" + end + doc = query.comments.map { |c| "# #{c.strip}".rstrip }.join("\n") + method = Code.block("def #{func}#{signature}", method_body(statements)) + doc.empty? ? method : "#{doc}\n#{method}" end def type(query) func = Code.snake_case(query.name) - params = if query.cmd == ':copyfrom' then "(Array[#{query.name}Args]) " - elsif query.params.empty? then '' - else "(#{query.name}Args) " - end - "\ndef #{func}: #{params}-> #{return_type(query)}" + if query.cmd == ':copyfrom' + signature = "(Array[#{query.name}Args] rows)" + else + list = params(query).map { |p| "#{p.optional ? '?' : ''}#{p.name}: #{p.type}" } + signature = list.empty? ? '' : "(#{list.join(', ')})" + end + "def #{func}: #{signature}#{signature.empty? ? '' : ' '}-> #{return_type(query)}" end # [[code, rbs]] of private helper methods the generated class needs for these queries. @@ -63,80 +86,69 @@ def last_id_type(query) @types.type(query.columns.first, query_name: query.name) end - def copy_statements(query) + def copy_statements(query, params) raise ArgumentError, "#{query.name}: :copyfrom without a target table" if query.insert_into_table.nil? table = query.insert_into_table name = table.schema.to_s.empty? ? table.name : "#{table.schema}.#{table.name}" - @driver.copy_from_lines(query, name, query.params.map { |p| p.column.name }) + @driver.copy_from_lines('rows', name, params.map { |p| p.column.name }, params.map(&:name), + Code.snake_case(query.name)) end - def query_statements(query, func, args_var) - has_params = !query.params.empty? + def query_statements(query, func, params) if slice?(query) && !@driver.supports_slice? raise ArgumentError, "#{query.name}: sqlc.slice is not supported by the #{@driver.name} driver" end statements = [] - statements << query_params(args_var, query.params) if has_params - sql_const = "#{query.name}Sql" + statements << query_params(params) unless params.empty? + const = Code.sql_constant(query.name) if slice?(query) - statements << slice_sql(sql_const, args_var, query.params) - sql_const = 'sql' + statements << slice_sql(const, params) + const = 'sql' end - statements << @driver.prepare_stmt(func, sql_const) - commands = command_statements(query, func, has_params) + stmt_name = "#{@statement_prefix}_#{func}" + prepare = @driver.prepare_stmt(stmt_name, const) + statements << prepare if prepare + commands = command_statements(query, stmt_name, const, !params.empty?) if @driver.closes_statements? - statements << Code.block('begin', commands.join("\n")).sub(/\nend\z/, "\nensure\n\tstmt.close\nend") + statements << Code.block('begin', commands.join("\n")).sub(/\nend\z/, "\nensure\n stmt.close\nend") else statements.concat(commands) end statements end - def query_params(args_var, params) - list = params.map do |p| - access = "#{args_var}.#{p.column.name}" - p.column.is_sqlc_slice ? "*#{access}" : access - end + def query_params(params) + list = params.map { |p| p.slice ? "*#{p.name}" : p.name } "query_params = [#{Code.comma_join(list)}]" end - # sql = FooSql.sub('/*SLICE:ids*/?', slice_placeholders(args.ids)).sub(...) - def slice_sql(sql_const, args_var, params) - subs = params.select { |p| p.column.is_sqlc_slice }.map do |p| - ".sub('/*SLICE:#{p.column.name}*/?', slice_placeholders(#{args_var}.#{p.column.name}))" - end - "sql = #{sql_const}#{subs.join}" + # sql = FOO_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - once per slice + def slice_sql(const, params) + params.select(&:slice).each_with_index.map do |p, i| + "sql = #{i.zero? ? const : 'sql'}.sub('/*SLICE:#{p.column.name}*/?', slice_placeholders(#{p.name}))" + end.join("\n") end - def command_statements(query, func, has_params) - array_rows = @layout.array_rows?(query) - exec = @driver.execute_stmt(func, has_params, array_rows: array_rows) + # The value of the last expression is the return value of the method (no explicit `return`). + def command_statements(query, stmt_name, const, has_params) + exec = @driver.execute_stmt(stmt_name, const, has_params, array_rows: @layout.array_rows?(query)) case query.cmd when ':one' - ["result = #{exec}", - 'row = result.first', - 'return nil if row.nil?', - "entity = #{new_row(query)}", - 'return entity'] + ["result = #{exec}", 'row = result.first', "row && #{new_row(query)}"] when ':many' - ["result = #{exec}", - 'entities = []', - Code.block('result.each do |row|', "entities << #{new_row(query)}"), - 'return entities'] - when ':exec' then [exec] + ["result = #{exec}", Code.block('result.map do |row|', new_row(query))] + when ':exec' then [exec, 'nil'] when ':execrows' then @driver.exec_rows_lines(exec) when ':execlastid' then @driver.exec_last_id_lines(exec, query) else raise ArgumentError, "unsupported query command: #{query.cmd}" end end - # :execlastid / :copyfrom use the pool even where :one/:many/:exec would not (they were pooled in the C# - # implementation too). - def method_body(statements, _cmd) + def method_body(statements) body = statements.join("\n") - return Code.block('@db.with do |client|', body) if @driver.pooling_enabled? + return Code.block(@driver.client_block, body) if @driver.pooling_enabled? "client = @db\n#{body}" end diff --git a/lib/sqlc_gen_ruby/options.rb b/lib/sqlc_gen_ruby/options.rb index 4c88afb..4f20013 100644 --- a/lib/sqlc_gen_ruby/options.rb +++ b/lib/sqlc_gen_ruby/options.rb @@ -11,7 +11,8 @@ class Options # `overrides:` entry - column is ":" or "*:"; type/not_null come from `ruby_type:` Override = Struct.new(:query, :column, :type, :not_null) - attr_reader :driver_name, :ruby_version, :generate_gemfile, :generate_types, :overrides + attr_reader :driver_name, :ruby_version, :generate_gemfile, :generate_types, :overrides, :module_name, + :prepared_statements def initialize(plugin_options_bytes) text = plugin_options_bytes.to_s.dup.force_encoding(Encoding::UTF_8) @@ -25,6 +26,8 @@ def initialize(plugin_options_bytes) @generate_types = fetch_bool(raw, 'generateTypes', true) @ruby_version = RubyVersion.parse(fetch_string(raw, 'rubyVersion', default: '3.3')) @overrides = parse_overrides(raw.fetch('overrides', [])) + @module_name = raw.key?('moduleName') ? fetch_module_name(raw) : nil # default: Codegen + @prepared_statements = fetch_bool(raw, 'preparedStatements', true) end # The override matching a result column / parameter of a query: "Query:col" wins over "*:col". @@ -65,6 +68,15 @@ def fetch_string(raw, key, default: nil, required: false) value end + def fetch_module_name(raw) + name = fetch_string(raw, 'moduleName') + unless name.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/) + raise ArgumentError, "option moduleName must be a Ruby constant path (e.g. MyApp::Db), got #{name.inspect}" + end + + name + end + def fetch_bool(raw, key, default) value = raw.fetch(key, default) raise ArgumentError, "option #{key} must be a boolean" unless [true, false].include?(value) diff --git a/lib/sqlc_gen_ruby/type_resolver.rb b/lib/sqlc_gen_ruby/type_resolver.rb index 32cdae2..9669777 100644 --- a/lib/sqlc_gen_ruby/type_resolver.rb +++ b/lib/sqlc_gen_ruby/type_resolver.rb @@ -17,6 +17,10 @@ def type(column, query_name: nil, fallback: :raise) not_null?(column, override) || base == 'untyped' ? base : "#{base}?" end + def nullable?(column, query_name) + !not_null?(column, @options.override_for(query_name, column.name)) && !column.is_sqlc_slice + end + # A sqlc.slice parameter is passed as an Array of the element type. def slice_type(column, query_name) override = @options.override_for(query_name, column.name) diff --git a/steep_expectations.yml b/steep_expectations.yml index 5cd7015..b98524e 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,31 +3,31 @@ diagnostics: - range: start: - line: 192 - character: 9 + line: 195 + character: 10 end: - line: 192 - character: 23 + line: 195 + character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 193 - character: 4 + line: 195 + character: 47 end: - line: 193 - character: 10 + line: 195 + character: 53 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 565 - character: 3 - end: - line: 565 + line: 580 character: 6 + end: + line: 580 + character: 9 severity: WARNING message: 'Cannot find the declaration of constant: `CSV`' code: Ruby::UnknownConstant @@ -35,61 +35,71 @@ diagnostics: - range: start: - line: 213 - character: 9 + line: 210 + character: 10 end: - line: 213 - character: 23 + line: 210 + character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 214 - character: 13 + line: 210 + character: 47 end: - line: 214 - character: 15 + line: 210 + character: 49 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 215 - character: 15 + line: 221 + character: 49 end: - line: 215 - character: 17 + line: 221 + character: 51 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 216 - character: 32 + line: 406 + character: 18 end: - line: 216 - character: 34 + line: 406 + character: 20 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 218 + line: 579 + character: 17 + end: + line: 579 + character: 19 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 580 character: 34 end: - line: 218 + line: 580 character: 36 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 422 - character: 14 + line: 582 + character: 36 end: - line: 422 - character: 16 + line: 582 + character: 38 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant diff --git a/test/codec_test.rb b/test/codec_test.rb index 49faa54..d84ea48 100644 --- a/test/codec_test.rb +++ b/test/codec_test.rb @@ -20,8 +20,8 @@ def test_official_request_and_response_agree_with_own_codec official = Fixture.k('GenerateResponse').decode(out).files.to_h { |f| [f.name, f.contents] } own = Plugin::GenerateResponse.decode(out).files.to_h { |f| [f.name, f.contents] } assert_equal official, own - assert_equal %w[query_sql.rb query_sql.rbs], own.keys.sort - assert_includes own.fetch('query_sql.rb'), 'GetAuthorSql = %q(SELECT id, name, bio FROM authors' + assert_includes own.keys, 'query_sql.rb' + assert_includes own.fetch('query_sql.rb'), "GET_AUTHOR_SQL = <<~'SQL'\n SELECT id, name, bio FROM authors" end def test_own_encoding_is_readable_by_the_official_implementation diff --git a/test/features_test.rb b/test/features_test.rb index 3e44946..863eb60 100644 --- a/test/features_test.rb +++ b/test/features_test.rb @@ -28,19 +28,19 @@ def test_invalid_overrides_are_rejected def test_array_parameters_and_columns rbs = generate(load_request('pg')).fetch('query_sql.rbs') - assert_includes rbs, 'attr_reader param1: Array[Integer]' + assert_includes rbs, 'def get_authors_by_ids: (param1: Array[Integer]) -> Array[GetAuthorsByIdsRow]' assert_includes rbs, 'attr_reader c_text_array: Array[String]?' end def test_unnamed_parameters_get_positional_names files = generate(load_request('pg')) - assert_includes files.fetch('query_sql.rb'), 'class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2)' + assert_includes files.fetch('query_sql.rb'), 'def get_authors_by_ids_and_names(param1:, param2:)' end def test_models_cover_tables_and_enums pg = generate(load_request('pg')) assert_includes pg.fetch('models.rb'), 'class Author < Data.define(:id, :name, :bio)' - assert_includes pg.fetch('models.rb'), "AUTOBIOGRAPHY = \"Autobiography\"" + assert_includes pg.fetch('models.rb'), "AUTOBIOGRAPHY = 'Autobiography'" assert_includes pg.fetch('models.rb'), 'module ExtendedBioType' assert_includes pg.fetch('models.rbs'), 'attr_reader bio_type: String?' assert_includes pg.fetch('query_sql.rb'), "require_relative 'models'" @@ -71,10 +71,10 @@ def test_embedded_tables_become_model_attributes def test_slices_expand_at_runtime_and_are_array_typed files = generate(load_request('mysql2')) - assert_includes files.fetch('query_sql.rb'), "sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids))" - assert_includes files.fetch('query_sql.rb'), 'query_params = [*get_authors_by_ids_args.ids]' - assert_includes files.fetch('query_sql.rbs'), 'attr_reader ids: Array[Integer]' - assert_includes files.fetch('query_sql.rbs'), 'def slice_placeholders: (Array[untyped]) -> String' + assert_includes files.fetch('query_sql.rb'), "sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids))" + assert_includes files.fetch('query_sql.rb'), 'query_params = [*ids]' + assert_includes files.fetch('query_sql.rbs'), 'def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow]' + assert_includes files.fetch('query_sql.rbs'), 'def slice_placeholders: (Array[untyped] values) -> String' end def test_slices_are_rejected_for_postgres @@ -83,14 +83,34 @@ def test_slices_are_rejected_for_postgres assert_raises(RuntimeError) { generate(request) } end - def test_command_return_types + def test_command_signatures rbs = generate(load_request('pg')).fetch('query_sql.rbs') - assert_includes rbs, 'def update_author_bios: (UpdateAuthorBiosArgs) -> Integer' - assert_includes rbs, 'def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer' - assert_includes rbs, 'def create_book: (CreateBookArgs) -> String' - assert_includes rbs, 'def copy_authors: (Array[CopyAuthorsArgs]) -> void' + assert_includes rbs, 'def get_author: (id: Integer) -> GetAuthorRow?' + assert_includes rbs, 'def update_author_bios: (bio: String?) -> Integer' + assert_includes rbs, 'def create_author_return_id: (name: String, bio: String?) -> Integer' + assert_includes rbs, 'def create_book: (name: String, author_id: Integer) -> String' + assert_includes rbs, 'def copy_authors: (Array[CopyAuthorsArgs] rows) -> void' assert_includes rbs, 'def list_authors: -> Array[ListAuthorsRow]' - assert_includes rbs, 'def transaction: [T] () { (self) -> T } -> T' + assert_includes rbs, 'def delete_author: (id: Integer) -> void' + assert_includes rbs, 'def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T' + end + + def test_named_nullable_parameters_are_optional_keywords + files = generate(load_request('pg')) + assert_includes files.fetch('query_sql.rb'), 'def get_author_by_name_pattern(name_pattern: nil)' + assert_includes files.fetch('query_sql.rbs'), 'def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow]' + # a plain nullable INSERT column has to be passed explicitly + assert_includes files.fetch('query_sql.rb'), 'def create_author_return_id(name:, bio:)' + end + + def test_reserved_words_and_camel_case_parameter_names + request = load_request('pg') + query(request, 'GetAuthor').params.first.column.name = 'class' + query(request, 'DeleteAuthor').params.first.column.name = 'authorId' + rb = generate(request).fetch('query_sql.rb') + assert_includes rb, 'def get_author(class_:)' + assert_includes rb, 'def delete_author(author_id:)' + assert_includes rb, 'query_params = [class_]' end def test_pg_and_sqlite_execlastid_need_returning @@ -111,7 +131,88 @@ def test_mysql_copyfrom_adds_csv_helpers_and_gemfile_entry def test_sqlite_and_mysql_close_prepared_statements %w[mysql2 sqlite3].each do |driver| - assert_match(/ensure\n\t+stmt\.close\n/, generate(load_request(driver)).fetch('query_sql.rb'), driver) + assert_match(/ensure\n\s+stmt\.close\n/, generate(load_request(driver)).fetch('query_sql.rb'), driver) + end + end + + # ---- Ruby style of the generated code ------------------------------------------------------------------------ + + def test_generated_ruby_is_formatted_like_ruby + %w[pg mysql2 sqlite3].each do |driver| + generate(load_request(driver)).each do |name, contents| + refute_includes contents, "\t", "#{driver}/#{name} contains tabs" + assert_equal contents.lines.first.chomp, '# frozen_string_literal: true' if name.end_with?('.rb') + assert contents.end_with?("\n"), "#{driver}/#{name} should end with a newline" + outside_heredocs = contents.gsub(/<<~'(\w+)'\n.*?^\s*\1$/m, 'HEREDOC') + refute_match(/[ ]+$/, outside_heredocs, "#{driver}/#{name} has trailing whitespace") + next unless name.end_with?('.rb') + + assert_equal 0, ruby_syntax_check(contents), "#{driver}/#{name} is not valid Ruby" + end end end + + def ruby_syntax_check(source) + _out, _err, status = Open3.capture3(RbConfig.ruby, '-c', stdin_data: source) + status.exitstatus + end + + def test_sql_is_a_frozen_raw_heredoc + rb = generate(load_request('pg')).fetch('query_sql.rb') + assert_includes rb, "GET_AUTHOR_SQL = <<~'SQL'\n SELECT id, name, bio FROM authors\n WHERE id = $1 LIMIT 1\n SQL" + end + + def test_heredoc_terminator_never_collides_with_the_query + request = load_request('sqlite3') + query(request, 'GetAuthor').text = "SELECT 1\nSQL\nFROM x" + assert_includes generate(request).fetch('query_sql.rb'), "<<~'SQL_'" + end + + def test_query_comments_become_doc_comments + rb = generate(load_request('pg')).fetch('query_sql.rb') + assert_includes rb, " # Returns the author with the given id, or nil when there is none.\n def get_author(id:)" + end + + def test_statements_are_named_after_the_query_class + rb = generate(load_request('pg')).fetch('query_sql.rb') + assert_includes rb, "client.exec_prepared('query_sql_get_author', query_params)" + end + + def test_pg_can_skip_prepared_statements + rb = generate(load_request('pg', preparedStatements: false)).fetch('query_sql.rb') + assert_includes rb, 'client.exec_params(GET_AUTHOR_SQL, query_params)' + refute_includes rb, 'client.prepare(' + end + + def test_module_name_option + files = generate(load_request('pg', moduleName: 'MyApp::Db')) + files.each { |name, contents| assert_includes contents, 'module MyApp::Db' if name.end_with?('.rb', '.rbs') } + assert_raises(RuntimeError) { generate(load_request('pg', moduleName: 'not a constant')) } + end + + def test_set_is_only_required_before_ruby_3_2 + assert_includes generate(load_request('pg', rubyVersion: '3.1')).fetch('query_sql.rb'), "require 'set'" + refute_includes generate(load_request('pg')).fetch('query_sql.rb'), "require 'set'" + end + + # Ruby < 3.2 has no Data: rows are Structs, whose members must be symbols (Struct.new(:id, :name)). + def test_struct_classes_are_valid_and_usable + files = generate(load_request('pg', rubyVersion: '3.1')) + assert_includes files.fetch('models.rb'), 'class Author < Struct.new(:id, :name, :bio)' + assert_includes files.fetch('query_sql.rb'), 'class GetAuthorRow < Struct.new(:id, :name, :bio)' + scope = Module.new + scope.module_eval(files.fetch('models.rb')) + author = scope::PgCodegen::Author.new(1, 'a', nil) + assert_equal({ id: 1, name: 'a', bio: nil }, author.to_h) + assert_equal 'a', author.name + end + + def test_data_classes_are_usable + files = generate(load_request('pg')) + scope = Module.new + scope.module_eval(files.fetch('models.rb')) + author = scope::PgCodegen::Author.new(id: 1, name: 'a', bio: nil) + assert_equal 'a', author.name + assert author.frozen? + end end diff --git a/test/fixtures/mysql2.bin b/test/fixtures/mysql2.bin index 719bc29..eef840c 100644 --- a/test/fixtures/mysql2.bin +++ b/test/fixtures/mysql2.bin @@ -70,13 +70,13 @@ c_longtext0 author_type0R extended_biosbextended_bios_author_type": extended_bios_bio_type Autobiography BiographyMemoir"7 extended_bios_author_typeAuthorEditor -Translatorˆ +TranslatorË 6SELECT id, name, bio FROM authors WHERE id = ? LIMIT 1 GetAuthor:one"* id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*.* -id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid: query.sqlÔ +id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid2A Returns the author with the given id, or nil when there is none.: query.sqlÔ /SELECT id, name, bio FROM authors ORDER BY name ListAuthors:many"* id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", @@ -179,11 +179,11 @@ WHERE name LIKE COALESCE(?, '%')GetAuthorByNamePattern:many"* id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*84 - name_pattern0ÿÿÿÿÿÿÿÿÿ8R authorsbtextzname: query.sql’ + name_pattern0ÿÿÿÿÿÿÿÿÿ8R authorsbtextzname: query.sqlá 0UPDATE authors SET bio = ? WHERE bio IS NOT NULLUpdateAuthorBios :execrows*62 -bio0ÿÿÿÿÿÿÿÿÿ8Rpublicauthorsbtextzbio: query.sqlœ +bio0ÿÿÿÿÿÿÿÿÿ8Rpublicauthorsbtextzbio2M Sets the bio of every author that has one and returns how many were changed.: query.sqlœ >SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)GetAuthorsByIds:many"* id0ÿÿÿÿÿÿÿÿÿR authorsbbigintzid", name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( @@ -194,10 +194,10 @@ WHERE bio IS NOT NULLUpdateAuthorBios :execrows*62 name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*3/ ids0ÿÿÿÿÿÿÿÿÿ8R authorsbbiginthzid*51 -names0ÿÿÿÿÿÿÿÿÿ8R authorsbtexthzname: query.sqlÍ +names0ÿÿÿÿÿÿÿÿÿ8R authorsbtexthzname: query.sqlþ -INSERT INTO authors (name, bio) VALUES (?, ?) CopyAuthors :copyfrom*84 name0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzname*40 -bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio: query.sqlB authors8 +bio0ÿÿÿÿÿÿÿÿÿRpublicauthorsbtextzbio2/ Bulk insert: pass an Array of CopyAuthorsArgs.: query.sqlB authors8 DELETE FROM authorsTruncateAuthors:exec: query.sqlÜ 1INSERT INTO books (name, author_id) VALUES (?, ?) CreateBook :execlastid*62 diff --git a/test/fixtures/pg.bin b/test/fixtures/pg.bin index efa9ec7b3c12c1b3a9d066480a4202ad378de25d..75b705d8be0ce34b52416a125a8d16f39fba001f 100644 GIT binary patch delta 254 zcmX>e+7-TmpOf*@W&uuCMrR{Og`m`u(xSX#g_4X^g~Za5jQk>n^30M9Fh4!BEHzId zGet)MD4LgZoOHWPF+iWJ@ z!o!!=J`k}98{9J8n#zC(@dT6X@6KTSC;T&;Wv#uz9wDU`!sZ$t z!H^SJSXfv&iKcvy_rCXbXY2E~Yo*@T6Q#y!hAD+o*drK%E`xD`$_8`Lgp!5xJ^*33G%d(eucK7xbM0Y-boA~ffa|8e6g zJZi2{?a_0ixu860pH5yo>ZiHrZ!XgzuFAy&EPIkL!cu!55y(Vzy&oF8Wt|=V`%DU- NG~3>3`_=#K{sP&{S7ZPH delta 52 zcmZ3i`apSuCnICmVlPI<$*Wm-HqT_5&&t?0*^;AWvNj9%W^vAiOpGTd@8sSB void - end - - class NodeMysqlType - attr_reader c_bit: String? - attr_reader c_tinyint: Integer? - attr_reader c_bool: Integer? - attr_reader c_boolean: Integer? - attr_reader c_smallint: Integer? - attr_reader c_mediumint: Integer? - attr_reader c_int: Integer? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer - attr_reader c_decimal: BigDecimal? - attr_reader c_dec: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_fixed: BigDecimal? - attr_reader c_float: Float? - attr_reader c_double: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: Time? - attr_reader c_datetime: Time? - attr_reader c_timestamp: Time? - attr_reader c_year: Integer? - attr_reader c_char: String? - attr_reader c_nchar: String? - attr_reader c_national_char: String? - attr_reader c_varchar: String? - attr_reader c_binary: String? - attr_reader c_varbinary: String? - attr_reader c_tinyblob: String? - attr_reader c_tinytext: String? - attr_reader c_blob: String? - attr_reader c_text: String? - attr_reader c_mediumblob: String? - attr_reader c_mediumtext: String? - attr_reader c_longblob: String? - attr_reader c_longtext: String? - attr_reader c_json: String? - def initialize: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> void - end - - class Book - attr_reader id: Integer - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( Integer id, String name, Integer author_id, String? description) -> void - end - - class ExtendedBio - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + module ExtendedBiosAuthorType + AUTHOR: String + EDITOR: String + TRANSLATOR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodeMysqlType + attr_reader c_bit: String? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? + attr_reader c_tinytext: String? + attr_reader c_blob: String? + attr_reader c_text: String? + attr_reader c_mediumblob: String? + attr_reader c_mediumtext: String? + attr_reader c_longblob: String? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end + + class ExtendedBio + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end +end diff --git a/test/golden/mysql2/default/my_queries_sql.rb b/test/golden/mysql2/default/my_queries_sql.rb index ed5335d..1099c73 100644 --- a/test/golden/mysql2/default/my_queries_sql.rb +++ b/test/golden/mysql2/default/my_queries_sql.rb @@ -1,46 +1,74 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/default/my_queries_sql.rbs b/test/golden/mysql2/default/my_queries_sql.rbs index 49ed0c8..798ac22 100644 --- a/test/golden/mysql2/default/my_queries_sql.rbs +++ b/test/golden/mysql2/default/my_queries_sql.rbs @@ -1,18 +1,18 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - class My_queriesSql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: Integer) -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + end +end diff --git a/test/golden/mysql2/default/other_sql.rb b/test/golden/mysql2/default/other_sql.rb index ffcdc25..048cedf 100644 --- a/test/golden/mysql2/default/other_sql.rb +++ b/test/golden/mysql2/default/other_sql.rb @@ -1,98 +1,119 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - ensure - stmt.close - end - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def other_one + with_client do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row['c0']) + ensure + stmt.close + end + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/default/other_sql.rbs b/test/golden/mysql2/default/other_sql.rbs index 784128b..c84282f 100644 --- a/test/golden/mysql2/default/other_sql.rbs +++ b/test/golden/mysql2/default/other_sql.rbs @@ -1,46 +1,31 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - NoColsSql: String - - class NoColsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - OtherOneSql: String - - class OtherOneRow - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - CountRowsSql: String - - class CountRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - class OtherSql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + COUNT_ROWS_SQL: String + + class OtherSql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: Integer) -> Integer + + private + + def with_client: [T] () { (untyped) -> T } -> T + end +end diff --git a/test/golden/mysql2/default/query_sql.rb b/test/golden/mysql2/default/query_sql.rb index c74b768..e96dcd6 100644 --- a/test/golden/mysql2/default/query_sql.rb +++ b/test/golden/mysql2/default/query_sql.rb @@ -1,669 +1,672 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' require 'csv' require 'tempfile' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Data.define(:bio, :id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ? OFFSET ?) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:limit, :offset) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio - FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - ensure - stmt.close - end - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - ensure - stmt.close - end - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - return if copy_authors_args.empty? - Tempfile.create(['copy_authors', '.csv']) do |file| - copy_authors_args.each do |item| - file.write(csv_line([item.name, item.bio])) - end - file.flush - client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") - end - end - end - - def truncate_authors - @db.with do |client| - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def list_all_authors_books - @db.with do |client| - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def get_duplicate_authors - @db.with do |client| - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params, as: :array) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] - stmt = client.prepare(CreateExtendedBioSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - stmt = client.prepare(GetFirstExtendedBioByTypeSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) - return entity - ensure - stmt.close - end - end - end - - def truncate_extended_bios - @db.with do |client| - stmt = client.prepare(TruncateExtendedBiosSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - ensure - stmt.close - end - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - ensure - stmt.close - end - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - ensure - stmt.close - end - end - end - - private - - def csv_line(values) - fields = values.map do |value| - case value - when nil then '\N' - when true then '1' - when false then '0' - when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') - else value.to_s.gsub('\\') { '\\\\' } - end - end - CSV.generate_line(fields) - end - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + UPDATE_AUTHOR_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE id = ? + SQL + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ? + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) + SQL + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + DELETE FROM extended_bios + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + ensure + stmt.close + end + end + end + + def list_authors + with_client do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def update_author(bio:, id:) + with_client do |client| + query_params = [bio, id] + stmt = client.prepare(UPDATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def test + with_client do |client| + stmt = client.prepare(TEST_SQL) + begin + result = stmt.execute + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + ensure + stmt.close + end + end + end + + def list_authors_paged(limit:, offset:) + with_client do |client| + query_params = [limit, offset] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio: nil) + with_client do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(ids:) + with_client do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + with_client do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + return if rows.empty? + + Tempfile.create(['copy_authors', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.name, row.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + nil + end + end + + def truncate_authors + with_client do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + with_client do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def get_duplicate_authors + with_client do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params, as: :array) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:, author_type:) + with_client do |client| + query_params = [author_name, name, bio_type, author_type] + stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + ensure + stmt.close + end + end + end + + def truncate_extended_bios + with_client do |client| + stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + ensure + stmt.close + end + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + + # Formats one row for LOAD DATA ... FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'. + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden/mysql2/default/query_sql.rbs b/test/golden/mysql2/default/query_sql.rbs index 3530b74..f5a0181 100644 --- a/test/golden/mysql2/default/query_sql.rbs +++ b/test/golden/mysql2/default/query_sql.rbs @@ -1,390 +1,296 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorSql: String - - class UpdateAuthorArgs - attr_reader bio: String? - attr_reader id: Integer - def initialize: ( String? bio, Integer id) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: String? - attr_reader c_tinyint: Integer? - attr_reader c_bool: Integer? - attr_reader c_boolean: Integer? - attr_reader c_smallint: Integer? - attr_reader c_mediumint: Integer? - attr_reader c_int: Integer? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer - attr_reader c_decimal: BigDecimal? - attr_reader c_dec: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_fixed: BigDecimal? - attr_reader c_float: Float? - attr_reader c_double: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: Time? - attr_reader c_datetime: Time? - attr_reader c_timestamp: Time? - attr_reader c_year: Integer? - attr_reader c_char: String? - attr_reader c_nchar: String? - attr_reader c_national_char: String? - attr_reader c_varchar: String? - attr_reader c_binary: String? - attr_reader c_varbinary: String? - attr_reader c_tinyblob: String? - attr_reader c_tinytext: String? - attr_reader c_blob: String? - attr_reader c_text: String? - attr_reader c_mediumblob: String? - attr_reader c_mediumtext: String? - attr_reader c_longblob: String? - attr_reader c_longtext: String? - attr_reader c_json: String? - def initialize: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader limit: Integer - attr_reader offset: Integer - def initialize: ( Integer limit, Integer offset) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader ids: Array[Integer] - def initialize: ( Array[Integer] ids) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader ids: Array[Integer] - attr_reader names: Array[String] - def initialize: ( Array[Integer] ids, Array[String] names) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow - attr_reader author_name: String? - attr_reader name: String? - attr_reader bio_type: String? - attr_reader author_type: String? - def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void - end - - class GetFirstExtendedBioByTypeArgs - attr_reader bio_type: String? - def initialize: ( String? bio_type) -> void - end - - TruncateExtendedBiosSql: String - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: Integer - attr_reader c1: String - def initialize: ( Integer c0, String c1) -> void - end - - class ManyParamsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - def initialize: ( Integer c0, String c1, Float c2, String c3) -> void - end - - class QuerySql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def update_author: (UpdateAuthorArgs) -> void - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - - private - def csv_line: (Array[untyped]) -> String - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + UPDATE_AUTHOR_SQL: String + + CREATE_AUTHOR_RETURN_ID_SQL: String + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow + attr_reader c_bit: String? + attr_reader c_tinyint: Integer? + attr_reader c_bool: Integer? + attr_reader c_boolean: Integer? + attr_reader c_smallint: Integer? + attr_reader c_mediumint: Integer? + attr_reader c_int: Integer? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer + attr_reader c_decimal: BigDecimal? + attr_reader c_dec: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_fixed: BigDecimal? + attr_reader c_float: Float? + attr_reader c_double: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: Time? + attr_reader c_datetime: Time? + attr_reader c_timestamp: Time? + attr_reader c_year: Integer? + attr_reader c_char: String? + attr_reader c_nchar: String? + attr_reader c_national_char: String? + attr_reader c_varchar: String? + attr_reader c_binary: String? + attr_reader c_varbinary: String? + attr_reader c_tinyblob: String? + attr_reader c_tinytext: String? + attr_reader c_blob: String? + attr_reader c_text: String? + attr_reader c_mediumblob: String? + attr_reader c_mediumtext: String? + attr_reader c_longblob: String? + attr_reader c_longtext: String? + attr_reader c_json: String? + def initialize: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> void + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String? + attr_reader name: String? + attr_reader bio_type: String? + attr_reader author_type: String? + def initialize: ( String? author_name, String? name, String? bio_type, String? author_type) -> void + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + MANY_PARAMS_SQL: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + MANY_ROWS_SQL: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: String + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + String c3, + Integer c4 + ) -> void + end + + FOUR_COLS_SQL: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: String + def initialize: ( Integer c0, String c1, Float c2, String c3) -> void + end + + class QuerySql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def update_author: (bio: String?, id: Integer) -> void + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (?bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String?, name: String?, bio_type: String?, author_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + + def many_rows: (c0: Integer) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def csv_line: (Array[untyped] values) -> String + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/connection.rb b/test/golden/mysql2/ruby31_gemfile/connection.rb new file mode 100644 index 0000000..9c5d70e --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/connection.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + # State kept on every Mysql2::Client the first time it is used. + module ConnectionState + attr_accessor :in_transaction + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/test/golden/mysql2/ruby31_gemfile/connection.rbs b/test/golden/mysql2/ruby31_gemfile/connection.rbs new file mode 100644 index 0000000..4af350d --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/connection.rbs @@ -0,0 +1,8 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + module ConnectionState + attr_accessor in_transaction: bool + end + + ISOLATION_LEVELS: Hash[Symbol, String] +end diff --git a/test/golden/mysql2/ruby31_gemfile/models.rb b/test/golden/mysql2/ruby31_gemfile/models.rb index 32b303b..7a5a36d 100644 --- a/test/golden/mysql2/ruby31_gemfile/models.rb +++ b/test/golden/mysql2/ruby31_gemfile/models.rb @@ -1,67 +1,69 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Mysql2Codegen - module ExtendedBiosBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - module ExtendedBiosAuthorType - AUTHOR = "Author" - EDITOR = "Editor" - TRANSLATOR = "Translator" - ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze - end - - class Author < Struct.new(id, name, bio) - end - - class NodeMysqlType < Struct.new( - c_bit, - c_tinyint, - c_bool, - c_boolean, - c_smallint, - c_mediumint, - c_int, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_dec, - c_numeric, - c_fixed, - c_float, - c_double, - c_double_precision, - c_date, - c_time, - c_datetime, - c_timestamp, - c_year, - c_char, - c_nchar, - c_national_char, - c_varchar, - c_binary, - c_varbinary, - c_tinyblob, - c_tinytext, - c_blob, - c_text, - c_mediumblob, - c_mediumtext, - c_longblob, - c_longtext, - c_json - ) - end - - class Book < Struct.new(id, name, author_id, description) - end - - class ExtendedBio < Struct.new(author_name, name, bio_type, author_type) - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = 'Author' + EDITOR = 'Editor' + TRANSLATOR = 'Translator' + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Struct.new(:id, :name, :bio) + end + + class NodeMysqlType < Struct.new( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + class Book < Struct.new(:id, :name, :author_id, :description) + end + + class ExtendedBio < Struct.new(:author_name, :name, :bio_type, :author_type) + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/models.rbs b/test/golden/mysql2/ruby31_gemfile/models.rbs index e3215f8..a728153 100644 --- a/test/golden/mysql2/ruby31_gemfile/models.rbs +++ b/test/golden/mysql2/ruby31_gemfile/models.rbs @@ -1,118 +1,118 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - module ExtendedBiosBioType - AUTOBIOGRAPHY: String - BIOGRAPHY: String - MEMOIR: String - ALL: Array[String] - end - - module ExtendedBiosAuthorType - AUTHOR: String - EDITOR: String - TRANSLATOR: String - ALL: Array[String] - end - - class Author < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> Author - end - - class NodeMysqlType < ::Struct - attr_accessor c_bit: String? - attr_accessor c_tinyint: Integer? - attr_accessor c_bool: Integer? - attr_accessor c_boolean: Integer? - attr_accessor c_smallint: Integer? - attr_accessor c_mediumint: Integer? - attr_accessor c_int: Integer? - attr_accessor c_integer: Integer? - attr_accessor c_bigint: Integer? - attr_accessor c_serial: Integer - attr_accessor c_decimal: BigDecimal? - attr_accessor c_dec: BigDecimal? - attr_accessor c_numeric: BigDecimal? - attr_accessor c_fixed: BigDecimal? - attr_accessor c_float: Float? - attr_accessor c_double: Float? - attr_accessor c_double_precision: Float? - attr_accessor c_date: Date? - attr_accessor c_time: Time? - attr_accessor c_datetime: Time? - attr_accessor c_timestamp: Time? - attr_accessor c_year: Integer? - attr_accessor c_char: String? - attr_accessor c_nchar: String? - attr_accessor c_national_char: String? - attr_accessor c_varchar: String? - attr_accessor c_binary: String? - attr_accessor c_varbinary: String? - attr_accessor c_tinyblob: String? - attr_accessor c_tinytext: String? - attr_accessor c_blob: String? - attr_accessor c_text: String? - attr_accessor c_mediumblob: String? - attr_accessor c_mediumtext: String? - attr_accessor c_longblob: String? - attr_accessor c_longtext: String? - attr_accessor c_json: String? - def self.new: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> NodeMysqlType - end - - class Book < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor author_id: Integer - attr_accessor description: String? - def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book - end - - class ExtendedBio < ::Struct - attr_accessor author_name: String? - attr_accessor name: String? - attr_accessor bio_type: String? - attr_accessor author_type: String? - def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> ExtendedBio - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + module ExtendedBiosAuthorType + AUTHOR: String + EDITOR: String + TRANSLATOR: String + ALL: Array[String] + end + + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class NodeMysqlType < ::Struct + attr_accessor c_bit: String? + attr_accessor c_tinyint: Integer? + attr_accessor c_bool: Integer? + attr_accessor c_boolean: Integer? + attr_accessor c_smallint: Integer? + attr_accessor c_mediumint: Integer? + attr_accessor c_int: Integer? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer + attr_accessor c_decimal: BigDecimal? + attr_accessor c_dec: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_fixed: BigDecimal? + attr_accessor c_float: Float? + attr_accessor c_double: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: Date? + attr_accessor c_time: Time? + attr_accessor c_datetime: Time? + attr_accessor c_timestamp: Time? + attr_accessor c_year: Integer? + attr_accessor c_char: String? + attr_accessor c_nchar: String? + attr_accessor c_national_char: String? + attr_accessor c_varchar: String? + attr_accessor c_binary: String? + attr_accessor c_varbinary: String? + attr_accessor c_tinyblob: String? + attr_accessor c_tinytext: String? + attr_accessor c_blob: String? + attr_accessor c_text: String? + attr_accessor c_mediumblob: String? + attr_accessor c_mediumtext: String? + attr_accessor c_longblob: String? + attr_accessor c_longtext: String? + attr_accessor c_json: String? + def self.new: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> NodeMysqlType + end + + class Book < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book + end + + class ExtendedBio < ::Struct + attr_accessor author_name: String? + attr_accessor name: String? + attr_accessor bio_type: String? + attr_accessor author_type: String? + def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> ExtendedBio + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb index 9af655d..1099c73 100644 --- a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb @@ -1,46 +1,74 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Struct.new(c0) - end - - class My_queriesSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs index e2e2e12..798ac22 100644 --- a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs @@ -1,18 +1,18 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> WithNumbers2GoArgs - end - - class My_queriesSql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: Integer) -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rb b/test/golden/mysql2/ruby31_gemfile/other_sql.rb index cfb13a0..1be00ad 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rb @@ -1,98 +1,119 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Struct.new(c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Struct.new(c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - ensure - stmt.close - end - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Struct.new(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def other_one + with_client do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row['c0']) + ensure + stmt.close + end + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs index cda3807..b3c8c10 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs @@ -1,46 +1,31 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - NoColsSql: String - - class NoColsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> NoColsArgs - end - - OtherOneSql: String - - class OtherOneRow < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> OtherOneRow - end - - CountRowsSql: String - - class CountRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> CountRowsArgs - end - - class OtherSql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> OtherOneRow + end + + COUNT_ROWS_SQL: String + + class OtherSql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: Integer) -> Integer + + private + + def with_client: [T] () { (untyped) -> T } -> T + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rb b/test/golden/mysql2/ruby31_gemfile/query_sql.rb index 5b82150..b5ce0ae 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rb @@ -1,669 +1,672 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' require 'csv' require 'tempfile' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Struct.new(name, bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Struct.new(bio, id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Struct.new(id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Struct.new( - c_bit, - c_tinyint, - c_bool, - c_boolean, - c_smallint, - c_mediumint, - c_int, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_dec, - c_numeric, - c_fixed, - c_float, - c_double, - c_double_precision, - c_date, - c_time, - c_datetime, - c_timestamp, - c_year, - c_char, - c_nchar, - c_national_char, - c_varchar, - c_binary, - c_varbinary, - c_tinyblob, - c_tinytext, - c_blob, - c_text, - c_mediumblob, - c_mediumtext, - c_longblob, - c_longtext, - c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ? OFFSET ?) - - class ListAuthorsPagedRow < Struct.new(id, name, bio) - end - - class ListAuthorsPagedArgs < Struct.new(limit, offset) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?, '%')) - - class GetAuthorByNamePatternRow < Struct.new(id, name, bio) - end - - class GetAuthorByNamePatternArgs < Struct.new(name_pattern) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Struct.new(bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsArgs < Struct.new(ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsAndNamesArgs < Struct.new(ids, names) - end - - class CopyAuthorsArgs < Struct.new(name, bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) - - class CreateBookArgs < Struct.new(name, author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Struct.new(author, book) - end - - GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio - FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Struct.new(author, author2) - end - - GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) - end - - class GetAuthorsByBookNameArgs < Struct.new(name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) - - class CreateExtendedBioArgs < Struct.new(author_name, name, bio_type, author_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Struct.new(author_name, name, bio_type, author_type) - end - - class GetFirstExtendedBioByTypeArgs < Struct.new(bio_type) - end - - TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - ensure - stmt.close - end - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - ensure - stmt.close - end - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - return if copy_authors_args.empty? - Tempfile.create(['copy_authors', '.csv']) do |file| - copy_authors_args.each do |item| - file.write(csv_line([item.name, item.bio])) - end - file.flush - client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") - end - end - end - - def truncate_authors - @db.with do |client| - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def list_all_authors_books - @db.with do |client| - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def get_duplicate_authors - @db.with do |client| - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params, as: :array) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] - stmt = client.prepare(CreateExtendedBioSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - stmt = client.prepare(GetFirstExtendedBioByTypeSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) - return entity - ensure - stmt.close - end - end - end - - def truncate_extended_bios - @db.with do |client| - stmt = client.prepare(TruncateExtendedBiosSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - ensure - stmt.close - end - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - ensure - stmt.close - end - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - ensure - stmt.close - end - end - end - - private - - def csv_line(values) - fields = values.map do |value| - case value - when nil then '\N' - when true then '1' - when false then '0' - when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') - else value.to_s.gsub('\\') { '\\\\' } - end - end - CSV.generate_line(fields) - end - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Struct.new(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Struct.new(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + UPDATE_AUTHOR_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE id = ? + SQL + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1 + SQL + + class TestRow < Struct.new( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ? + SQL + + class ListAuthorsPagedRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%') + SQL + + class GetAuthorByNamePatternRow < Struct.new(:id, :name, :bio) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Struct.new(:id, :name, :bio) + end + + class CopyAuthorsArgs < Struct.new(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) + SQL + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Struct.new(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Struct.new(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Struct.new(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Struct.new(:author_name, :name, :bio_type, :author_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + DELETE FROM extended_bios + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Struct.new(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Struct.new( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + ensure + stmt.close + end + end + end + + def list_authors + with_client do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def update_author(bio:, id:) + with_client do |client| + query_params = [bio, id] + stmt = client.prepare(UPDATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def test + with_client do |client| + stmt = client.prepare(TEST_SQL) + begin + result = stmt.execute + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + ensure + stmt.close + end + end + end + + def list_authors_paged(limit:, offset:) + with_client do |client| + query_params = [limit, offset] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio: nil) + with_client do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(ids:) + with_client do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + with_client do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + return if rows.empty? + + Tempfile.create(['copy_authors', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.name, row.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + nil + end + end + + def truncate_authors + with_client do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + with_client do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def get_duplicate_authors + with_client do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params, as: :array) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:, author_type:) + with_client do |client| + query_params = [author_name, name, bio_type, author_type] + stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + ensure + stmt.close + end + end + end + + def truncate_extended_bios + with_client do |client| + stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + ensure + stmt.close + end + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + + # Formats one row for LOAD DATA ... FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'. + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs index 0a5fd89..8bedc4a 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -1,390 +1,296 @@ # auto-generated by sqlc - do not edit module Mysql2Codegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorArgs - end - - UpdateAuthorSql: String - - class UpdateAuthorArgs < ::Struct - attr_accessor bio: String? - attr_accessor id: Integer - def self.new: ( String? bio, Integer id) -> UpdateAuthorArgs - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - TestSql: String - - class TestRow < ::Struct - attr_accessor c_bit: String? - attr_accessor c_tinyint: Integer? - attr_accessor c_bool: Integer? - attr_accessor c_boolean: Integer? - attr_accessor c_smallint: Integer? - attr_accessor c_mediumint: Integer? - attr_accessor c_int: Integer? - attr_accessor c_integer: Integer? - attr_accessor c_bigint: Integer? - attr_accessor c_serial: Integer - attr_accessor c_decimal: BigDecimal? - attr_accessor c_dec: BigDecimal? - attr_accessor c_numeric: BigDecimal? - attr_accessor c_fixed: BigDecimal? - attr_accessor c_float: Float? - attr_accessor c_double: Float? - attr_accessor c_double_precision: Float? - attr_accessor c_date: Date? - attr_accessor c_time: Time? - attr_accessor c_datetime: Time? - attr_accessor c_timestamp: Time? - attr_accessor c_year: Integer? - attr_accessor c_char: String? - attr_accessor c_nchar: String? - attr_accessor c_national_char: String? - attr_accessor c_varchar: String? - attr_accessor c_binary: String? - attr_accessor c_varbinary: String? - attr_accessor c_tinyblob: String? - attr_accessor c_tinytext: String? - attr_accessor c_blob: String? - attr_accessor c_text: String? - attr_accessor c_mediumblob: String? - attr_accessor c_mediumtext: String? - attr_accessor c_longblob: String? - attr_accessor c_longtext: String? - attr_accessor c_json: String? - def self.new: ( - String? c_bit, - Integer? c_tinyint, - Integer? c_bool, - Integer? c_boolean, - Integer? c_smallint, - Integer? c_mediumint, - Integer? c_int, - Integer? c_integer, - Integer? c_bigint, - Integer c_serial, - BigDecimal? c_decimal, - BigDecimal? c_dec, - BigDecimal? c_numeric, - BigDecimal? c_fixed, - Float? c_float, - Float? c_double, - Float? c_double_precision, - Date? c_date, - Time? c_time, - Time? c_datetime, - Time? c_timestamp, - Integer? c_year, - String? c_char, - String? c_nchar, - String? c_national_char, - String? c_varchar, - String? c_binary, - String? c_varbinary, - String? c_tinyblob, - String? c_tinytext, - String? c_blob, - String? c_text, - String? c_mediumblob, - String? c_mediumtext, - String? c_longblob, - String? c_longtext, - String? c_json - ) -> TestRow - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow - end - - class ListAuthorsPagedArgs < ::Struct - attr_accessor limit: Integer - attr_accessor offset: Integer - def self.new: ( Integer limit, Integer offset) -> ListAuthorsPagedArgs - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow - end - - class GetAuthorByNamePatternArgs < ::Struct - attr_accessor name_pattern: String? - def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs < ::Struct - attr_accessor bio: String? - def self.new: ( String? bio) -> UpdateAuthorBiosArgs - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow - end - - class GetAuthorsByIdsArgs < ::Struct - attr_accessor ids: Array[Integer] - def self.new: ( Array[Integer] ids) -> GetAuthorsByIdsArgs - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow - end - - class GetAuthorsByIdsAndNamesArgs < ::Struct - attr_accessor ids: Array[Integer] - attr_accessor names: Array[String] - def self.new: ( Array[Integer] ids, Array[String] names) -> GetAuthorsByIdsAndNamesArgs - end - - class CopyAuthorsArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CopyAuthorsArgs - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookArgs < ::Struct - attr_accessor name: String - attr_accessor author_id: Integer - def self.new: ( String name, Integer author_id) -> CreateBookArgs - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow < ::Struct - attr_accessor author: Author - attr_accessor book: Book - def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow < ::Struct - attr_accessor author: Author - attr_accessor author2: Author - def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - attr_accessor book: Book - def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow - end - - class GetAuthorsByBookNameArgs < ::Struct - attr_accessor name: String - def self.new: ( String name) -> GetAuthorsByBookNameArgs - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs < ::Struct - attr_accessor author_name: String? - attr_accessor name: String? - attr_accessor bio_type: String? - attr_accessor author_type: String? - def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> CreateExtendedBioArgs - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow < ::Struct - attr_accessor author_name: String? - attr_accessor name: String? - attr_accessor bio_type: String? - attr_accessor author_type: String? - def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> GetFirstExtendedBioByTypeRow - end - - class GetFirstExtendedBioByTypeArgs < ::Struct - attr_accessor bio_type: String? - def self.new: ( String? bio_type) -> GetFirstExtendedBioByTypeArgs - end - - TruncateExtendedBiosSql: String - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - def self.new: ( Integer c0, String c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow - end - - class QuerySql - @db: untyped - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def update_author: (UpdateAuthorArgs) -> void - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - - private - def csv_line: (Array[untyped]) -> String - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CREATE_AUTHOR_SQL: String + + UPDATE_AUTHOR_SQL: String + + CREATE_AUTHOR_RETURN_ID_SQL: String + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow < ::Struct + attr_accessor c_bit: String? + attr_accessor c_tinyint: Integer? + attr_accessor c_bool: Integer? + attr_accessor c_boolean: Integer? + attr_accessor c_smallint: Integer? + attr_accessor c_mediumint: Integer? + attr_accessor c_int: Integer? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer + attr_accessor c_decimal: BigDecimal? + attr_accessor c_dec: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_fixed: BigDecimal? + attr_accessor c_float: Float? + attr_accessor c_double: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: Date? + attr_accessor c_time: Time? + attr_accessor c_datetime: Time? + attr_accessor c_timestamp: Time? + attr_accessor c_year: Integer? + attr_accessor c_char: String? + attr_accessor c_nchar: String? + attr_accessor c_national_char: String? + attr_accessor c_varchar: String? + attr_accessor c_binary: String? + attr_accessor c_varbinary: String? + attr_accessor c_tinyblob: String? + attr_accessor c_tinytext: String? + attr_accessor c_blob: String? + attr_accessor c_text: String? + attr_accessor c_mediumblob: String? + attr_accessor c_mediumtext: String? + attr_accessor c_longblob: String? + attr_accessor c_longtext: String? + attr_accessor c_json: String? + def self.new: ( + String? c_bit, + Integer? c_tinyint, + Integer? c_bool, + Integer? c_boolean, + Integer? c_smallint, + Integer? c_mediumint, + Integer? c_int, + Integer? c_integer, + Integer? c_bigint, + Integer c_serial, + BigDecimal? c_decimal, + BigDecimal? c_dec, + BigDecimal? c_numeric, + BigDecimal? c_fixed, + Float? c_float, + Float? c_double, + Float? c_double_precision, + Date? c_date, + Time? c_time, + Time? c_datetime, + Time? c_timestamp, + Integer? c_year, + String? c_char, + String? c_nchar, + String? c_national_char, + String? c_varchar, + String? c_binary, + String? c_varbinary, + String? c_tinyblob, + String? c_tinytext, + String? c_blob, + String? c_text, + String? c_mediumblob, + String? c_mediumtext, + String? c_longblob, + String? c_longtext, + String? c_json + ) -> TestRow + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow < ::Struct + attr_accessor author_name: String? + attr_accessor name: String? + attr_accessor bio_type: String? + attr_accessor author_type: String? + def self.new: ( String? author_name, String? name, String? bio_type, String? author_type) -> GetFirstExtendedBioByTypeRow + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + MANY_PARAMS_SQL: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + MANY_ROWS_SQL: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: String + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + String c3, + Integer c4 + ) -> ManyRowsRow + end + + FOUR_COLS_SQL: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: String + def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow + end + + class QuerySql + def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def update_author: (bio: String?, id: Integer) -> void + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def update_author_bios: (?bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String?, name: String?, bio_type: String?, author_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + + def many_rows: (c0: Integer) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def csv_line: (Array[untyped] values) -> String + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/test/golden/mysql2/ruby32_notypes/connection.rb b/test/golden/mysql2/ruby32_notypes/connection.rb new file mode 100644 index 0000000..9c5d70e --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/connection.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module Mysql2Codegen + # State kept on every Mysql2::Client the first time it is used. + module ConnectionState + attr_accessor :in_transaction + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/test/golden/mysql2/ruby32_notypes/models.rb b/test/golden/mysql2/ruby32_notypes/models.rb index 627f01b..3c5c04f 100644 --- a/test/golden/mysql2/ruby32_notypes/models.rb +++ b/test/golden/mysql2/ruby32_notypes/models.rb @@ -1,67 +1,69 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Mysql2Codegen - module ExtendedBiosBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - module ExtendedBiosAuthorType - AUTHOR = "Author" - EDITOR = "Editor" - TRANSLATOR = "Translator" - ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze - end - - class Author < Data.define(:id, :name, :bio) - end - - class NodeMysqlType < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end - - class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) - end -end \ No newline at end of file + module ExtendedBiosBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + module ExtendedBiosAuthorType + AUTHOR = 'Author' + EDITOR = 'Editor' + TRANSLATOR = 'Translator' + ALL = [AUTHOR, EDITOR, TRANSLATOR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodeMysqlType < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type, :author_type) + end +end diff --git a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb index ed5335d..1099c73 100644 --- a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb @@ -1,46 +1,74 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/ruby32_notypes/other_sql.rb b/test/golden/mysql2/ruby32_notypes/other_sql.rb index ffcdc25..048cedf 100644 --- a/test/golden/mysql2/ruby32_notypes/other_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/other_sql.rb @@ -1,98 +1,119 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def other_one - @db.with do |client| - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - ensure - stmt.close - end - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def other_one + with_client do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row['c0']) + ensure + stmt.close + end + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + end +end diff --git a/test/golden/mysql2/ruby32_notypes/query_sql.rb b/test/golden/mysql2/ruby32_notypes/query_sql.rb index c74b768..e96dcd6 100644 --- a/test/golden/mysql2/ruby32_notypes/query_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/query_sql.rb @@ -1,669 +1,672 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'mysql2' require 'csv' require 'tempfile' +require_relative 'connection' require_relative 'models' module Mysql2Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - UpdateAuthorSql = %q(UPDATE authors - SET bio = ? - WHERE id = ?) - - class UpdateAuthorArgs < Data.define(:bio, :id) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_tinyint, - :c_bool, - :c_boolean, - :c_smallint, - :c_mediumint, - :c_int, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_dec, - :c_numeric, - :c_fixed, - :c_float, - :c_double, - :c_double_precision, - :c_date, - :c_time, - :c_datetime, - :c_timestamp, - :c_year, - :c_char, - :c_nchar, - :c_national_char, - :c_varchar, - :c_binary, - :c_varbinary, - :c_tinyblob, - :c_tinytext, - :c_blob, - :c_text, - :c_mediumblob, - :c_mediumtext, - :c_longblob, - :c_longtext, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ? OFFSET ?) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:limit, :offset) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?)) - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio - FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description - FROM authors JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type, :author_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(DELETE FROM extended_bios) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, mysql2_params) - @db = ConnectionPool.new(**connection_pool_params) { - Mysql2::Client.new(**mysql2_params) - } - end - - def transaction - @db.with do |client| - client.query('BEGIN') - begin - result = yield self - client.query('COMMIT') - result - rescue Exception - client.query('ROLLBACK') - raise - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - ensure - stmt.close - end - end - end - - def list_authors - @db.with do |client| - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def update_author(update_author_args) - @db.with do |client| - query_params = [update_author_args.bio, update_author_args.id] - stmt = client.prepare(UpdateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def test - @db.with do |client| - stmt = client.prepare(TestSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_tinyint'], - row['c_bool'], - row['c_boolean'], - row['c_smallint'], - row['c_mediumint'], - row['c_int'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_dec'], - row['c_numeric'], - row['c_fixed'], - row['c_float'], - row['c_double'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_datetime'], - row['c_timestamp'], - row['c_year'], - row['c_char'], - row['c_nchar'], - row['c_national_char'], - row['c_varchar'], - row['c_binary'], - row['c_varbinary'], - row['c_tinyblob'], - row['c_tinytext'], - row['c_blob'], - row['c_text'], - row['c_mediumblob'], - row['c_mediumtext'], - row['c_longblob'], - row['c_longtext'], - row['c_json'] - ) - return entity - ensure - stmt.close - end - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.limit, list_authors_paged_args.offset] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return stmt.affected_rows - ensure - stmt.close - end - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - ensure - stmt.close - end - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - return if copy_authors_args.empty? - Tempfile.create(['copy_authors', '.csv']) do |file| - copy_authors_args.each do |item| - file.write(csv_line([item.name, item.bio])) - end - file.flush - client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") - end - end - end - - def truncate_authors - @db.with do |client| - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - stmt.execute(*query_params) - return client.last_id - ensure - stmt.close - end - end - end - - def list_all_authors_books - @db.with do |client| - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def get_duplicate_authors - @db.with do |client| - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute(as: :array) - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params, as: :array) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type, create_extended_bio_args.author_type] - stmt = client.prepare(CreateExtendedBioSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - stmt = client.prepare(GetFirstExtendedBioByTypeSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) - return entity - ensure - stmt.close - end - end - end - - def truncate_extended_bios - @db.with do |client| - stmt = client.prepare(TruncateExtendedBiosSql) - begin - stmt.execute - ensure - stmt.close - end - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - ensure - stmt.close - end - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - ensure - stmt.close - end - end - end - - def four_cols - @db.with do |client| - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - ensure - stmt.close - end - end - end - - private - - def csv_line(values) - fields = values.map do |value| - case value - when nil then '\N' - when true then '1' - when false then '0' - when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') - else value.to_s.gsub('\\') { '\\\\' } - end - end - CSV.generate_line(fields) - end - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + UPDATE_AUTHOR_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE id = ? + SQL + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_tinyint, c_bool, c_boolean, c_smallint, c_mediumint, c_int, c_integer, c_bigint, c_serial, c_decimal, c_dec, c_numeric, c_fixed, c_float, c_double, c_double_precision, c_date, c_time, c_datetime, c_timestamp, c_year, c_char, c_nchar, c_national_char, c_varchar, c_binary, c_varbinary, c_tinyblob, c_tinytext, c_blob, c_text, c_mediumblob, c_mediumtext, c_longblob, c_longtext, c_json FROM node_mysql_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_tinyint, + :c_bool, + :c_boolean, + :c_smallint, + :c_mediumint, + :c_int, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_dec, + :c_numeric, + :c_fixed, + :c_float, + :c_double, + :c_double_precision, + :c_date, + :c_time, + :c_datetime, + :c_timestamp, + :c_year, + :c_char, + :c_nchar, + :c_national_char, + :c_varchar, + :c_binary, + :c_varbinary, + :c_tinyblob, + :c_tinytext, + :c_blob, + :c_text, + :c_mediumblob, + :c_mediumtext, + :c_longblob, + :c_longtext, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ? OFFSET ? + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) + SQL + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT authors1.id, authors1.name, authors1.bio, authors2.id, authors2.name, authors2.bio + FROM authors authors1 JOIN authors authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT authors.id, authors.name, authors.bio, books.id, books.name, books.author_id, books.description + FROM authors JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended_bios (author_name, name, bio_type, author_type) VALUES (?, ?, ?, ?) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type, author_type FROM extended_bios WHERE bio_type = ? LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, :author_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + DELETE FROM extended_bios + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = !client.in_transaction + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.query("SET TRANSACTION ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}") if isolation + client.query('BEGIN') + client.in_transaction = true + else + client.query("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.query(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.query(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + ensure + client.in_transaction = false if outermost + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + ensure + stmt.close + end + end + end + + def list_authors + with_client do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def update_author(bio:, id:) + with_client do |client| + query_params = [bio, id] + stmt = client.prepare(UPDATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def test + with_client do |client| + stmt = client.prepare(TEST_SQL) + begin + result = stmt.execute + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_tinyint'], + row['c_bool'], + row['c_boolean'], + row['c_smallint'], + row['c_mediumint'], + row['c_int'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_dec'], + row['c_numeric'], + row['c_fixed'], + row['c_float'], + row['c_double'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_datetime'], + row['c_timestamp'], + row['c_year'], + row['c_char'], + row['c_nchar'], + row['c_national_char'], + row['c_varchar'], + row['c_binary'], + row['c_varbinary'], + row['c_tinyblob'], + row['c_tinytext'], + row['c_blob'], + row['c_text'], + row['c_mediumblob'], + row['c_mediumtext'], + row['c_longblob'], + row['c_longtext'], + row['c_json'] + ) + ensure + stmt.close + end + end + end + + def list_authors_paged(limit:, offset:) + with_client do |client| + query_params = [limit, offset] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio: nil) + with_client do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + stmt.affected_rows + ensure + stmt.close + end + end + end + + def get_authors_by_ids(ids:) + with_client do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + with_client do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + return if rows.empty? + + Tempfile.create(['copy_authors', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.name, row.bio])) + end + file.flush + client.query("LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE authors FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (name, bio)") + end + nil + end + end + + def truncate_authors + with_client do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + stmt.execute(*query_params) + client.last_id + ensure + stmt.close + end + end + end + + def list_all_authors_books + with_client do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def get_duplicate_authors + with_client do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute(as: :array) + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params, as: :array) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:, author_type:) + with_client do |client| + query_params = [author_name, name, bio_type, author_type] + stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type'], row['author_type']) + ensure + stmt.close + end + end + end + + def truncate_extended_bios + with_client do |client| + stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + ensure + stmt.close + end + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + ensure + stmt.close + end + end + end + + private + + def with_client + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + end + + # Formats one row for LOAD DATA ... FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'. + def csv_line(values) + fields = values.map do |value| + case value + when nil then '\N' + when true then '1' + when false then '0' + when Time then value.strftime('%Y-%m-%d %H:%M:%S.%6N') + else value.to_s.gsub('\\') { '\\\\' } + end + end + CSV.generate_line(fields) + end + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden/pg/default/connection.rb b/test/golden/pg/default/connection.rb new file mode 100644 index 0000000..a6a4492 --- /dev/null +++ b/test/golden/pg/default/connection.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module PgCodegen + # State kept on every PG::Connection the first time it is used. + module ConnectionState + def prepared_statements + @prepared_statements ||= Set[] + end + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/test/golden/pg/default/connection.rbs b/test/golden/pg/default/connection.rbs new file mode 100644 index 0000000..3b51951 --- /dev/null +++ b/test/golden/pg/default/connection.rbs @@ -0,0 +1,8 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ConnectionState + def prepared_statements: () -> Set[String] + end + + ISOLATION_LEVELS: Hash[Symbol, String] +end diff --git a/test/golden/pg/default/models.rb b/test/golden/pg/default/models.rb index 9539cb7..6d0eee8 100644 --- a/test/golden/pg/default/models.rb +++ b/test/golden/pg/default/models.rb @@ -1,53 +1,55 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - class Author < Data.define(:id, :name, :bio) - end - - class NodePostgresType < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end - - class PostgresExtraType < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - class ExtendedBio < Data.define(:author_name, :name, :bio_type) - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end diff --git a/test/golden/pg/default/models.rbs b/test/golden/pg/default/models.rbs index cec018c..b97a45b 100644 --- a/test/golden/pg/default/models.rbs +++ b/test/golden/pg/default/models.rbs @@ -1,93 +1,93 @@ # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY: String - BIOGRAPHY: String - MEMOIR: String - ALL: Array[String] - end - - class Author - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class NodePostgresType - attr_reader c_bit: String? - attr_reader c_smallint: Integer? - attr_reader c_boolean: bool? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer? - attr_reader c_decimal: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_real: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: String? - attr_reader c_timestamp: Time? - attr_reader c_char: String? - attr_reader c_varchar: String? - attr_reader c_bytea: String? - attr_reader c_text: String? - attr_reader c_json: untyped - def initialize: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> void - end - - class Book - attr_reader id: String - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( String id, String name, Integer author_id, String? description) -> void - end - - class PostgresExtraType - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - class ExtendedBio - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class NodePostgresType + attr_reader c_bit: String? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: String? + attr_reader c_timestamp: Time? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: String? + attr_reader c_text: String? + attr_reader c_json: untyped + def initialize: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> void + end + + class Book + attr_reader id: String + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( String id, String name, Integer author_id, String? description) -> void + end + + class PostgresExtraType + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + class ExtendedBio + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end +end diff --git a/test/golden/pg/default/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb index 5cb8646..b56f64e 100644 --- a/test/golden/pg/default/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -1,47 +1,78 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('with_numbers2_go') - client.prepare('with_numbers2_go', WithNumbers2GoSql) - prepared.add('with_numbers2_go') - end - client.exec_prepared('with_numbers2_go', query_params) - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') + client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) + client.prepared_statements.add('my_queries_sql_with_numbers2_go') + end + client.exec_prepared('my_queries_sql_with_numbers2_go', query_params) + nil + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/default/my_queries_sql.rbs b/test/golden/pg/default/my_queries_sql.rbs index 2cfe350..8a4818c 100644 --- a/test/golden/pg/default/my_queries_sql.rbs +++ b/test/golden/pg/default/my_queries_sql.rbs @@ -1,19 +1,20 @@ # auto-generated by sqlc - do not edit module PgCodegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs - attr_reader c0: bool - def initialize: ( bool c0) -> void - end - - class My_queriesSql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: bool) -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/default/other_sql.rb b/test/golden/pg/default/other_sql.rb index 59b7056..1a6fb1f 100644 --- a/test/golden/pg/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -1,99 +1,121 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('no_cols') - client.prepare('no_cols', NoColsSql) - prepared.add('no_cols') - end - client.exec_prepared('no_cols', query_params) - end - end - - def other_one - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('other_one') - client.prepare('other_one', OtherOneSql) - prepared.add('other_one') - end - result = client.exec_prepared('other_one') - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('count_rows') - client.prepare('count_rows', CountRowsSql) - prepared.add('count_rows') - end - result = client.exec_prepared('count_rows', query_params) - return result.cmd_tuples - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('other_sql_no_cols') + client.prepare('other_sql_no_cols', NO_COLS_SQL) + client.prepared_statements.add('other_sql_no_cols') + end + client.exec_prepared('other_sql_no_cols', query_params) + nil + end + end + + def other_one + with_client do |client| + unless client.prepared_statements.include?('other_sql_other_one') + client.prepare('other_sql_other_one', OTHER_ONE_SQL) + client.prepared_statements.add('other_sql_other_one') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('other_sql_count_rows') + client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) + client.prepared_statements.add('other_sql_count_rows') + end + result = client.exec_prepared('other_sql_count_rows', query_params) + result.cmd_tuples + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/default/other_sql.rbs b/test/golden/pg/default/other_sql.rbs index 264bd6f..ae3ba27 100644 --- a/test/golden/pg/default/other_sql.rbs +++ b/test/golden/pg/default/other_sql.rbs @@ -1,47 +1,33 @@ # auto-generated by sqlc - do not edit module PgCodegen - NoColsSql: String - - class NoColsArgs - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - attr_reader c4: String - def initialize: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> void - end - - OtherOneSql: String - - class OtherOneRow - attr_reader c0: bool - def initialize: ( bool c0) -> void - end - - CountRowsSql: String - - class CountRowsArgs - attr_reader c0: bool - def initialize: ( bool c0) -> void - end - - class OtherSql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow + attr_reader c0: bool + def initialize: ( bool c0) -> void + end + + COUNT_ROWS_SQL: String + + class OtherSql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def no_cols: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: bool) -> Integer + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb index 60af7b6..aa15016 100644 --- a/test/golden/pg/default/query_sql.rb +++ b/test/golden/pg/default/query_sql.rb @@ -1,696 +1,668 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Data.define(:id, :name, :bio) - end - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT $2 - OFFSET $1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE($1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = $1 - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT [])) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:param1) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors - INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = $1) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) - - InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) - - class InsertExtraTypesArgs < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) - - class GetExtraTypesRow < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author') - client.prepare('get_author', GetAuthorSql) - prepared.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - prepared.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - prepared.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - prepared.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('test') - client.prepare('test', TestSql) - prepared.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors_paged') - client.prepare('list_authors_paged', ListAuthorsPagedSql) - prepared.add('list_authors_paged') - end - result = client.exec_prepared('list_authors_paged', query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author_by_name_pattern') - client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - prepared.add('get_author_by_name_pattern') - end - result = client.exec_prepared('get_author_by_name_pattern', query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author_return_id') - client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - prepared.add('create_author_return_id') - end - result = client.exec_prepared('create_author_return_id', query_params) - return result.first['id'] - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('update_author_bios') - client.prepare('update_author_bios', UpdateAuthorBiosSql) - prepared.add('update_author_bios') - end - result = client.exec_prepared('update_author_bios', query_params) - return result.cmd_tuples - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [get_authors_by_ids_args.param1] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids') - client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - prepared.add('get_authors_by_ids') - end - result = client.exec_prepared('get_authors_by_ids', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids_and_names') - client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - prepared.add('get_authors_by_ids_and_names') - end - result = client.exec_prepared('get_authors_by_ids_and_names', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) - client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do - copy_authors_args.each do |item| - client.put_copy_data([item.name, item.bio]) - end - end - end - end - - def truncate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_authors') - client.prepare('truncate_authors', TruncateAuthorsSql) - prepared.add('truncate_authors') - end - client.exec_prepared('truncate_authors') - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_book') - client.prepare('create_book', CreateBookSql) - prepared.add('create_book') - end - result = client.exec_prepared('create_book', query_params) - return result.first['id'] - end - end - - def list_all_authors_books - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_all_authors_books') - client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - prepared.add('list_all_authors_books') - end - result = client.exec_prepared('list_all_authors_books').values - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def get_duplicate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_duplicate_authors') - client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - prepared.add('get_duplicate_authors') - end - result = client.exec_prepared('get_duplicate_authors').values - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_book_name') - client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - prepared.add('get_authors_by_book_name') - end - result = client.exec_prepared('get_authors_by_book_name', query_params).values - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_extended_bio') - client.prepare('create_extended_bio', CreateExtendedBioSql) - prepared.add('create_extended_bio') - end - client.exec_prepared('create_extended_bio', query_params) - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_first_extended_bio_by_type') - client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - prepared.add('get_first_extended_bio_by_type') - end - result = client.exec_prepared('get_first_extended_bio_by_type', query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) - return entity - end - end - - def truncate_extended_bios - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extended_bios') - client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - prepared.add('truncate_extended_bios') - end - client.exec_prepared('truncate_extended_bios') - end - end - - def insert_extra_types(insert_extra_types_args) - @db.with do |client| - query_params = [ - insert_extra_types_args.c_uuid, - insert_extra_types_args.c_jsonb, - insert_extra_types_args.c_text_array, - insert_extra_types_args.c_integer_array, - insert_extra_types_args.c_timestamptz, - insert_extra_types_args.c_inet, - insert_extra_types_args.c_money, - insert_extra_types_args.c_not_null - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('insert_extra_types') - client.prepare('insert_extra_types', InsertExtraTypesSql) - prepared.add('insert_extra_types') - end - client.exec_prepared('insert_extra_types', query_params) - end - end - - def get_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_extra_types') - client.prepare('get_extra_types', GetExtraTypesSql) - prepared.add('get_extra_types') - end - result = client.exec_prepared('get_extra_types') - row = result.first - return nil if row.nil? - entity = GetExtraTypesRow.new( - row['c_uuid'], - row['c_jsonb'], - row['c_text_array'], - row['c_integer_array'], - row['c_timestamptz'], - row['c_inet'], - row['c_money'], - row['c_not_null'] - ) - return entity - end - end - - def truncate_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extra_types') - client.prepare('truncate_extra_types', TruncateExtraTypesSql) - prepared.add('truncate_extra_types') - end - client.exec_prepared('truncate_extra_types') - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_params') - client.prepare('many_params', ManyParamsSql) - prepared.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - prepared.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('four_cols') - client.prepare('four_cols', FourColsSql) - prepared.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio + SQL + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = $1 + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + TRUNCATE TABLE authors CASCADE + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1 + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + TRUNCATE TABLE extended.bios + SQL + + INSERT_EXTRA_TYPES_SQL = <<~'SQL' + INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + SQL + + GET_EXTRA_TYPES_SQL = <<~'SQL' + SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1 + SQL + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TRUNCATE_EXTRA_TYPES_SQL = <<~'SQL' + TRUNCATE TABLE postgres_extra_types + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_get_author') + client.prepare('query_sql_get_author', GET_AUTHOR_SQL) + client.prepared_statements.add('query_sql_get_author') + end + result = client.exec_prepared('query_sql_get_author', query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def list_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_authors') + client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) + client.prepared_statements.add('query_sql_list_authors') + end + result = client.exec_prepared('query_sql_list_authors') + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author') + client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_create_author') + end + result = client.exec_prepared('query_sql_create_author', query_params) + row = result.first + row && CreateAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_delete_author') + client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_delete_author') + end + client.exec_prepared('query_sql_delete_author', query_params) + nil + end + end + + def test + with_client do |client| + unless client.prepared_statements.include?('query_sql_test') + client.prepare('query_sql_test', TEST_SQL) + client.prepared_statements.add('query_sql_test') + end + result = client.exec_prepared('query_sql_test') + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + end + end + + def list_authors_paged(offset:, limit:) + with_client do |client| + query_params = [offset, limit] + unless client.prepared_statements.include?('query_sql_list_authors_paged') + client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) + client.prepared_statements.add('query_sql_list_authors_paged') + end + result = client.exec_prepared('query_sql_list_authors_paged', query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') + client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) + client.prepared_statements.add('query_sql_get_author_by_name_pattern') + end + result = client.exec_prepared('query_sql_get_author_by_name_pattern', query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author_return_id') + client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) + client.prepared_statements.add('query_sql_create_author_return_id') + end + result = client.exec_prepared('query_sql_create_author_return_id', query_params) + result.first['id'] + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + with_client do |client| + query_params = [bio] + unless client.prepared_statements.include?('query_sql_update_author_bios') + client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) + client.prepared_statements.add('query_sql_update_author_bios') + end + result = client.exec_prepared('query_sql_update_author_bios', query_params) + result.cmd_tuples + end + end + + def get_authors_by_ids(param1:) + with_client do |client| + query_params = [param1] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids') + client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids') + end + result = client.exec_prepared('query_sql_get_authors_by_ids', query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_authors_by_ids_and_names(param1:, param2:) + with_client do |client| + query_params = [param1, param2] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') + client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids_and_names') + end + result = client.exec_prepared('query_sql_get_authors_by_ids_and_names', query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_authors') + client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_truncate_authors') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + unless client.prepared_statements.include?('query_sql_create_book') + client.prepare('query_sql_create_book', CREATE_BOOK_SQL) + client.prepared_statements.add('query_sql_create_book') + end + result = client.exec_prepared('query_sql_create_book', query_params) + result.first['id'] + end + end + + def list_all_authors_books + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_all_authors_books') + client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) + client.prepared_statements.add('query_sql_list_all_authors_books') + end + result = client.exec_prepared('query_sql_list_all_authors_books').values + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def get_duplicate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_duplicate_authors') + client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_get_duplicate_authors') + end + result = client.exec_prepared('query_sql_get_duplicate_authors').values + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') + client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) + client.prepared_statements.add('query_sql_get_authors_by_book_name') + end + result = client.exec_prepared('query_sql_get_authors_by_book_name', query_params).values + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:) + with_client do |client| + query_params = [author_name, name, bio_type] + unless client.prepared_statements.include?('query_sql_create_extended_bio') + client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) + client.prepared_statements.add('query_sql_create_extended_bio') + end + client.exec_prepared('query_sql_create_extended_bio', query_params) + nil + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') + client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + client.prepared_statements.add('query_sql_get_first_extended_bio_by_type') + end + result = client.exec_prepared('query_sql_get_first_extended_bio_by_type', query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + end + end + + def truncate_extended_bios + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extended_bios') + client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) + client.prepared_statements.add('query_sql_truncate_extended_bios') + end + client.exec_prepared('query_sql_truncate_extended_bios') + nil + end + end + + def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) + with_client do |client| + query_params = [ + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ] + unless client.prepared_statements.include?('query_sql_insert_extra_types') + client.prepare('query_sql_insert_extra_types', INSERT_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_insert_extra_types') + end + client.exec_prepared('query_sql_insert_extra_types', query_params) + nil + end + end + + def get_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_extra_types') + client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_get_extra_types') + end + result = client.exec_prepared('query_sql_get_extra_types') + row = result.first + row && GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + end + end + + def truncate_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extra_types') + client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_truncate_extra_types') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('query_sql_many_params') + client.prepare('query_sql_many_params', MANY_PARAMS_SQL) + client.prepared_statements.add('query_sql_many_params') + end + result = client.exec_prepared('query_sql_many_params', query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('query_sql_many_rows') + client.prepare('query_sql_many_rows', MANY_ROWS_SQL) + client.prepared_statements.add('query_sql_many_rows') + end + result = client.exec_prepared('query_sql_many_rows', query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + end + end + + def four_cols + with_client do |client| + unless client.prepared_statements.include?('query_sql_four_cols') + client.prepare('query_sql_four_cols', FOUR_COLS_SQL) + client.prepared_statements.add('query_sql_four_cols') + end + result = client.exec_prepared('query_sql_four_cols') + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs index a5f6d04..fdc8fbf 100644 --- a/test/golden/pg/default/query_sql.rbs +++ b/test/golden/pg/default/query_sql.rbs @@ -1,408 +1,301 @@ # auto-generated by sqlc - do not edit module PgCodegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - TestSql: String - - class TestRow - attr_reader c_bit: String? - attr_reader c_smallint: Integer? - attr_reader c_boolean: bool? - attr_reader c_integer: Integer? - attr_reader c_bigint: Integer? - attr_reader c_serial: Integer? - attr_reader c_decimal: BigDecimal? - attr_reader c_numeric: BigDecimal? - attr_reader c_real: Float? - attr_reader c_double_precision: Float? - attr_reader c_date: Date? - attr_reader c_time: String? - attr_reader c_timestamp: Time? - attr_reader c_char: String? - attr_reader c_varchar: String? - attr_reader c_bytea: String? - attr_reader c_text: String? - attr_reader c_json: untyped - def initialize: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader offset: Integer - attr_reader limit: Integer - def initialize: ( Integer offset, Integer limit) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader param1: Array[Integer] - def initialize: ( Array[Integer] param1) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader param1: Array[Integer] - attr_reader param2: Array[String] - def initialize: ( Array[Integer] param1, Array[String] param2) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow - attr_reader id: String - def initialize: ( String id) -> void - end - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow - attr_reader author_name: String - attr_reader name: String - attr_reader bio_type: String? - def initialize: ( String author_name, String name, String? bio_type) -> void - end - - class GetFirstExtendedBioByTypeArgs - attr_reader bio_type: String? - def initialize: ( String? bio_type) -> void - end - - TruncateExtendedBiosSql: String - - InsertExtraTypesSql: String - - class InsertExtraTypesArgs - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - GetExtraTypesSql: String - - class GetExtraTypesRow - attr_reader c_uuid: String? - attr_reader c_jsonb: untyped - attr_reader c_text_array: Array[String]? - attr_reader c_integer_array: Array[Integer]? - attr_reader c_timestamptz: Time? - attr_reader c_inet: IPAddr? - attr_reader c_money: String? - attr_reader c_not_null: String - def initialize: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> void - end - - TruncateExtraTypesSql: String - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: bool - attr_reader c1: Integer - def initialize: ( bool c0, Integer c1) -> void - end - - class ManyParamsArgs - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - attr_reader c4: String - def initialize: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - attr_reader c4: String - def initialize: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: bool - def initialize: ( bool c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: bool - attr_reader c1: Integer - attr_reader c2: String - attr_reader c3: Float - def initialize: ( bool c0, Integer c1, String c2, Float c3) -> void - end - - class QuerySql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> CreateAuthorRow? - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> String - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - def insert_extra_types: (InsertExtraTypesArgs) -> void - - def get_extra_types: -> GetExtraTypesRow? - - def truncate_extra_types: -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + class CreateAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow + attr_reader c_bit: String? + attr_reader c_smallint: Integer? + attr_reader c_boolean: bool? + attr_reader c_integer: Integer? + attr_reader c_bigint: Integer? + attr_reader c_serial: Integer? + attr_reader c_decimal: BigDecimal? + attr_reader c_numeric: BigDecimal? + attr_reader c_real: Float? + attr_reader c_double_precision: Float? + attr_reader c_date: Date? + attr_reader c_time: String? + attr_reader c_timestamp: Time? + attr_reader c_char: String? + attr_reader c_varchar: String? + attr_reader c_bytea: String? + attr_reader c_text: String? + attr_reader c_json: untyped + def initialize: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> void + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow + attr_reader id: String + def initialize: ( String id) -> void + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow + attr_reader author_name: String + attr_reader name: String + attr_reader bio_type: String? + def initialize: ( String author_name, String name, String? bio_type) -> void + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + INSERT_EXTRA_TYPES_SQL: String + + GET_EXTRA_TYPES_SQL: String + + class GetExtraTypesRow + attr_reader c_uuid: String? + attr_reader c_jsonb: untyped + attr_reader c_text_array: Array[String]? + attr_reader c_integer_array: Array[Integer]? + attr_reader c_timestamptz: Time? + attr_reader c_inet: IPAddr? + attr_reader c_money: String? + attr_reader c_not_null: String + def initialize: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> void + end + + TRUNCATE_EXTRA_TYPES_SQL: String + + MANY_PARAMS_SQL: String + + class ManyParamsRow + attr_reader c0: bool + attr_reader c1: Integer + def initialize: ( bool c0, Integer c1) -> void + end + + MANY_ROWS_SQL: String + + class ManyRowsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + attr_reader c4: String + def initialize: ( + bool c0, + Integer c1, + String c2, + Float c3, + String c4 + ) -> void + end + + FOUR_COLS_SQL: String + + class FourColsRow + attr_reader c0: bool + attr_reader c1: Integer + attr_reader c2: String + attr_reader c3: Float + def initialize: ( bool c0, Integer c1, String c2, Float c3) -> void + end + + class QuerySql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> CreateAuthorRow? + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (param1: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (param1: Array[Integer], param2: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String, name: String, bio_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void + + def many_params: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow? + + def many_rows: (c0: bool) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/ruby31_gemfile/Gemfile b/test/golden/pg/ruby31_gemfile/Gemfile index be6f7f1..6b58541 100644 --- a/test/golden/pg/ruby31_gemfile/Gemfile +++ b/test/golden/pg/ruby31_gemfile/Gemfile @@ -1,5 +1,4 @@ source 'https://rubygems.org' gem 'connection_pool' -gem 'pg' -gem 'set' \ No newline at end of file +gem 'pg' \ No newline at end of file diff --git a/test/golden/pg/ruby31_gemfile/connection.rb b/test/golden/pg/ruby31_gemfile/connection.rb new file mode 100644 index 0000000..a6a4492 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/connection.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module PgCodegen + # State kept on every PG::Connection the first time it is used. + module ConnectionState + def prepared_statements + @prepared_statements ||= Set[] + end + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/test/golden/pg/ruby31_gemfile/connection.rbs b/test/golden/pg/ruby31_gemfile/connection.rbs new file mode 100644 index 0000000..3b51951 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/connection.rbs @@ -0,0 +1,8 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + module ConnectionState + def prepared_statements: () -> Set[String] + end + + ISOLATION_LEVELS: Hash[Symbol, String] +end diff --git a/test/golden/pg/ruby31_gemfile/models.rb b/test/golden/pg/ruby31_gemfile/models.rb index 32666cb..e3d3438 100644 --- a/test/golden/pg/ruby31_gemfile/models.rb +++ b/test/golden/pg/ruby31_gemfile/models.rb @@ -1,53 +1,55 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - class Author < Struct.new(id, name, bio) - end - - class NodePostgresType < Struct.new( - c_bit, - c_smallint, - c_boolean, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_numeric, - c_real, - c_double_precision, - c_date, - c_time, - c_timestamp, - c_char, - c_varchar, - c_bytea, - c_text, - c_json - ) - end - - class Book < Struct.new(id, name, author_id, description) - end - - class PostgresExtraType < Struct.new( - c_uuid, - c_jsonb, - c_text_array, - c_integer_array, - c_timestamptz, - c_inet, - c_money, - c_not_null - ) - end - - class ExtendedBio < Struct.new(author_name, name, bio_type) - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Struct.new(:id, :name, :bio) + end + + class NodePostgresType < Struct.new( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Struct.new(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Struct.new( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Struct.new(:author_name, :name, :bio_type) + end +end diff --git a/test/golden/pg/ruby31_gemfile/models.rbs b/test/golden/pg/ruby31_gemfile/models.rbs index 2912850..9d9bf3c 100644 --- a/test/golden/pg/ruby31_gemfile/models.rbs +++ b/test/golden/pg/ruby31_gemfile/models.rbs @@ -1,93 +1,93 @@ # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY: String - BIOGRAPHY: String - MEMOIR: String - ALL: Array[String] - end - - class Author < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> Author - end - - class NodePostgresType < ::Struct - attr_accessor c_bit: String? - attr_accessor c_smallint: Integer? - attr_accessor c_boolean: bool? - attr_accessor c_integer: Integer? - attr_accessor c_bigint: Integer? - attr_accessor c_serial: Integer? - attr_accessor c_decimal: BigDecimal? - attr_accessor c_numeric: BigDecimal? - attr_accessor c_real: Float? - attr_accessor c_double_precision: Float? - attr_accessor c_date: Date? - attr_accessor c_time: String? - attr_accessor c_timestamp: Time? - attr_accessor c_char: String? - attr_accessor c_varchar: String? - attr_accessor c_bytea: String? - attr_accessor c_text: String? - attr_accessor c_json: untyped - def self.new: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> NodePostgresType - end - - class Book < ::Struct - attr_accessor id: String - attr_accessor name: String - attr_accessor author_id: Integer - attr_accessor description: String? - def self.new: ( String id, String name, Integer author_id, String? description) -> Book - end - - class PostgresExtraType < ::Struct - attr_accessor c_uuid: String? - attr_accessor c_jsonb: untyped - attr_accessor c_text_array: Array[String]? - attr_accessor c_integer_array: Array[Integer]? - attr_accessor c_timestamptz: Time? - attr_accessor c_inet: IPAddr? - attr_accessor c_money: String? - attr_accessor c_not_null: String - def self.new: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> PostgresExtraType - end - - class ExtendedBio < ::Struct - attr_accessor author_name: String - attr_accessor name: String - attr_accessor bio_type: String? - def self.new: ( String author_name, String name, String? bio_type) -> ExtendedBio - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY: String + BIOGRAPHY: String + MEMOIR: String + ALL: Array[String] + end + + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class NodePostgresType < ::Struct + attr_accessor c_bit: String? + attr_accessor c_smallint: Integer? + attr_accessor c_boolean: bool? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_real: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: Date? + attr_accessor c_time: String? + attr_accessor c_timestamp: Time? + attr_accessor c_char: String? + attr_accessor c_varchar: String? + attr_accessor c_bytea: String? + attr_accessor c_text: String? + attr_accessor c_json: untyped + def self.new: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> NodePostgresType + end + + class Book < ::Struct + attr_accessor id: String + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( String id, String name, Integer author_id, String? description) -> Book + end + + class PostgresExtraType < ::Struct + attr_accessor c_uuid: String? + attr_accessor c_jsonb: untyped + attr_accessor c_text_array: Array[String]? + attr_accessor c_integer_array: Array[Integer]? + attr_accessor c_timestamptz: Time? + attr_accessor c_inet: IPAddr? + attr_accessor c_money: String? + attr_accessor c_not_null: String + def self.new: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> PostgresExtraType + end + + class ExtendedBio < ::Struct + attr_accessor author_name: String + attr_accessor name: String + attr_accessor bio_type: String? + def self.new: ( String author_name, String name, String? bio_type) -> ExtendedBio + end +end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb index 9b1269e..5464449 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -1,47 +1,79 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Struct.new(c0) - end - - class My_queriesSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('with_numbers2_go') - client.prepare('with_numbers2_go', WithNumbers2GoSql) - prepared.add('with_numbers2_go') - end - client.exec_prepared('with_numbers2_go', query_params) - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') + client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) + client.prepared_statements.add('my_queries_sql_with_numbers2_go') + end + client.exec_prepared('my_queries_sql_with_numbers2_go', query_params) + nil + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs index 147f94d..8a4818c 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs @@ -1,19 +1,20 @@ # auto-generated by sqlc - do not edit module PgCodegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs < ::Struct - attr_accessor c0: bool - def self.new: ( bool c0) -> WithNumbers2GoArgs - end - - class My_queriesSql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: bool) -> void + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb index 4520b46..c92eb76 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -1,99 +1,122 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Struct.new(c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Struct.new(c0) - end - - class OtherSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('no_cols') - client.prepare('no_cols', NoColsSql) - prepared.add('no_cols') - end - client.exec_prepared('no_cols', query_params) - end - end - - def other_one - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('other_one') - client.prepare('other_one', OtherOneSql) - prepared.add('other_one') - end - result = client.exec_prepared('other_one') - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('count_rows') - client.prepare('count_rows', CountRowsSql) - prepared.add('count_rows') - end - result = client.exec_prepared('count_rows', query_params) - return result.cmd_tuples - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Struct.new(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('other_sql_no_cols') + client.prepare('other_sql_no_cols', NO_COLS_SQL) + client.prepared_statements.add('other_sql_no_cols') + end + client.exec_prepared('other_sql_no_cols', query_params) + nil + end + end + + def other_one + with_client do |client| + unless client.prepared_statements.include?('other_sql_other_one') + client.prepare('other_sql_other_one', OTHER_ONE_SQL) + client.prepared_statements.add('other_sql_other_one') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('other_sql_count_rows') + client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) + client.prepared_statements.add('other_sql_count_rows') + end + result = client.exec_prepared('other_sql_count_rows', query_params) + result.cmd_tuples + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rbs b/test/golden/pg/ruby31_gemfile/other_sql.rbs index 3c402bc..582ff48 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -1,47 +1,33 @@ # auto-generated by sqlc - do not edit module PgCodegen - NoColsSql: String - - class NoColsArgs < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - attr_accessor c4: String - def self.new: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> NoColsArgs - end - - OtherOneSql: String - - class OtherOneRow < ::Struct - attr_accessor c0: bool - def self.new: ( bool c0) -> OtherOneRow - end - - CountRowsSql: String - - class CountRowsArgs < ::Struct - attr_accessor c0: bool - def self.new: ( bool c0) -> CountRowsArgs - end - - class OtherSql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow < ::Struct + attr_accessor c0: bool + def self.new: ( bool c0) -> OtherOneRow + end + + COUNT_ROWS_SQL: String + + class OtherSql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def no_cols: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: bool) -> Integer + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb index adb72a6..565cbc2 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rb +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -1,696 +1,669 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Struct.new(id, name, bio) - end - - class CreateAuthorArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Struct.new(id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Struct.new( - c_bit, - c_smallint, - c_boolean, - c_integer, - c_bigint, - c_serial, - c_decimal, - c_numeric, - c_real, - c_double_precision, - c_date, - c_time, - c_timestamp, - c_char, - c_varchar, - c_bytea, - c_text, - c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT $2 - OFFSET $1) - - class ListAuthorsPagedRow < Struct.new(id, name, bio) - end - - class ListAuthorsPagedArgs < Struct.new(offset, limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE($1, '%')) - - class GetAuthorByNamePatternRow < Struct.new(id, name, bio) - end - - class GetAuthorByNamePatternArgs < Struct.new(name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) - - class CreateAuthorReturnIdRow < Struct.new(id) - end - - class CreateAuthorReturnIdArgs < Struct.new(name, bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = $1 - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Struct.new(bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT [])) - - class GetAuthorsByIdsRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsArgs < Struct.new(param1) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) - - class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsAndNamesArgs < Struct.new(param1, param2) - end - - class CopyAuthorsArgs < Struct.new(name, bio) - end - - TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) - - class CreateBookRow < Struct.new(id) - end - - class CreateBookArgs < Struct.new(name, author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors - INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Struct.new(author, book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Struct.new(author, author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = $1) - - class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) - end - - class GetAuthorsByBookNameArgs < Struct.new(name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) - - class CreateExtendedBioArgs < Struct.new(author_name, name, bio_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Struct.new(author_name, name, bio_type) - end - - class GetFirstExtendedBioByTypeArgs < Struct.new(bio_type) - end - - TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) - - InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) - - class InsertExtraTypesArgs < Struct.new( - c_uuid, - c_jsonb, - c_text_array, - c_integer_array, - c_timestamptz, - c_inet, - c_money, - c_not_null - ) - end - - GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) - - class GetExtraTypesRow < Struct.new( - c_uuid, - c_jsonb, - c_text_array, - c_integer_array, - c_timestamptz, - c_inet, - c_money, - c_not_null - ) - end - - TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author') - client.prepare('get_author', GetAuthorSql) - prepared.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - prepared.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - prepared.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - prepared.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('test') - client.prepare('test', TestSql) - prepared.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors_paged') - client.prepare('list_authors_paged', ListAuthorsPagedSql) - prepared.add('list_authors_paged') - end - result = client.exec_prepared('list_authors_paged', query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author_by_name_pattern') - client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - prepared.add('get_author_by_name_pattern') - end - result = client.exec_prepared('get_author_by_name_pattern', query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author_return_id') - client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - prepared.add('create_author_return_id') - end - result = client.exec_prepared('create_author_return_id', query_params) - return result.first['id'] - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('update_author_bios') - client.prepare('update_author_bios', UpdateAuthorBiosSql) - prepared.add('update_author_bios') - end - result = client.exec_prepared('update_author_bios', query_params) - return result.cmd_tuples - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [get_authors_by_ids_args.param1] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids') - client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - prepared.add('get_authors_by_ids') - end - result = client.exec_prepared('get_authors_by_ids', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids_and_names') - client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - prepared.add('get_authors_by_ids_and_names') - end - result = client.exec_prepared('get_authors_by_ids_and_names', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) - client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do - copy_authors_args.each do |item| - client.put_copy_data([item.name, item.bio]) - end - end - end - end - - def truncate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_authors') - client.prepare('truncate_authors', TruncateAuthorsSql) - prepared.add('truncate_authors') - end - client.exec_prepared('truncate_authors') - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_book') - client.prepare('create_book', CreateBookSql) - prepared.add('create_book') - end - result = client.exec_prepared('create_book', query_params) - return result.first['id'] - end - end - - def list_all_authors_books - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_all_authors_books') - client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - prepared.add('list_all_authors_books') - end - result = client.exec_prepared('list_all_authors_books').values - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def get_duplicate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_duplicate_authors') - client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - prepared.add('get_duplicate_authors') - end - result = client.exec_prepared('get_duplicate_authors').values - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_book_name') - client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - prepared.add('get_authors_by_book_name') - end - result = client.exec_prepared('get_authors_by_book_name', query_params).values - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_extended_bio') - client.prepare('create_extended_bio', CreateExtendedBioSql) - prepared.add('create_extended_bio') - end - client.exec_prepared('create_extended_bio', query_params) - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_first_extended_bio_by_type') - client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - prepared.add('get_first_extended_bio_by_type') - end - result = client.exec_prepared('get_first_extended_bio_by_type', query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) - return entity - end - end - - def truncate_extended_bios - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extended_bios') - client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - prepared.add('truncate_extended_bios') - end - client.exec_prepared('truncate_extended_bios') - end - end - - def insert_extra_types(insert_extra_types_args) - @db.with do |client| - query_params = [ - insert_extra_types_args.c_uuid, - insert_extra_types_args.c_jsonb, - insert_extra_types_args.c_text_array, - insert_extra_types_args.c_integer_array, - insert_extra_types_args.c_timestamptz, - insert_extra_types_args.c_inet, - insert_extra_types_args.c_money, - insert_extra_types_args.c_not_null - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('insert_extra_types') - client.prepare('insert_extra_types', InsertExtraTypesSql) - prepared.add('insert_extra_types') - end - client.exec_prepared('insert_extra_types', query_params) - end - end - - def get_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_extra_types') - client.prepare('get_extra_types', GetExtraTypesSql) - prepared.add('get_extra_types') - end - result = client.exec_prepared('get_extra_types') - row = result.first - return nil if row.nil? - entity = GetExtraTypesRow.new( - row['c_uuid'], - row['c_jsonb'], - row['c_text_array'], - row['c_integer_array'], - row['c_timestamptz'], - row['c_inet'], - row['c_money'], - row['c_not_null'] - ) - return entity - end - end - - def truncate_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extra_types') - client.prepare('truncate_extra_types', TruncateExtraTypesSql) - prepared.add('truncate_extra_types') - end - client.exec_prepared('truncate_extra_types') - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_params') - client.prepare('many_params', ManyParamsSql) - prepared.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - prepared.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('four_cols') - client.prepare('four_cols', FourColsSql) - prepared.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1 + SQL + + class GetAuthorRow < Struct.new(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Struct.new(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio + SQL + + class CreateAuthorRow < Struct.new(:id, :name, :bio) + end + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = $1 + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1 + SQL + + class TestRow < Struct.new( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1 + SQL + + class ListAuthorsPagedRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%') + SQL + + class GetAuthorByNamePatternRow < Struct.new(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Struct.new(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) + SQL + + class GetAuthorsByIdsRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []) + SQL + + class GetAuthorsByIdsAndNamesRow < Struct.new(:id, :name, :bio) + end + + class CopyAuthorsArgs < Struct.new(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + TRUNCATE TABLE authors CASCADE + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id + SQL + + class CreateBookRow < Struct.new(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Struct.new(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Struct.new(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1 + SQL + + class GetAuthorsByBookNameRow < Struct.new(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Struct.new(:author_name, :name, :bio_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + TRUNCATE TABLE extended.bios + SQL + + INSERT_EXTRA_TYPES_SQL = <<~'SQL' + INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + SQL + + GET_EXTRA_TYPES_SQL = <<~'SQL' + SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1 + SQL + + class GetExtraTypesRow < Struct.new( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TRUNCATE_EXTRA_TYPES_SQL = <<~'SQL' + TRUNCATE TABLE postgres_extra_types + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Struct.new(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Struct.new( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_get_author') + client.prepare('query_sql_get_author', GET_AUTHOR_SQL) + client.prepared_statements.add('query_sql_get_author') + end + result = client.exec_prepared('query_sql_get_author', query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def list_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_authors') + client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) + client.prepared_statements.add('query_sql_list_authors') + end + result = client.exec_prepared('query_sql_list_authors') + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author') + client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_create_author') + end + result = client.exec_prepared('query_sql_create_author', query_params) + row = result.first + row && CreateAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_delete_author') + client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_delete_author') + end + client.exec_prepared('query_sql_delete_author', query_params) + nil + end + end + + def test + with_client do |client| + unless client.prepared_statements.include?('query_sql_test') + client.prepare('query_sql_test', TEST_SQL) + client.prepared_statements.add('query_sql_test') + end + result = client.exec_prepared('query_sql_test') + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + end + end + + def list_authors_paged(offset:, limit:) + with_client do |client| + query_params = [offset, limit] + unless client.prepared_statements.include?('query_sql_list_authors_paged') + client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) + client.prepared_statements.add('query_sql_list_authors_paged') + end + result = client.exec_prepared('query_sql_list_authors_paged', query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') + client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) + client.prepared_statements.add('query_sql_get_author_by_name_pattern') + end + result = client.exec_prepared('query_sql_get_author_by_name_pattern', query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author_return_id') + client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) + client.prepared_statements.add('query_sql_create_author_return_id') + end + result = client.exec_prepared('query_sql_create_author_return_id', query_params) + result.first['id'] + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + with_client do |client| + query_params = [bio] + unless client.prepared_statements.include?('query_sql_update_author_bios') + client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) + client.prepared_statements.add('query_sql_update_author_bios') + end + result = client.exec_prepared('query_sql_update_author_bios', query_params) + result.cmd_tuples + end + end + + def get_authors_by_ids(param1:) + with_client do |client| + query_params = [param1] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids') + client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids') + end + result = client.exec_prepared('query_sql_get_authors_by_ids', query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_authors_by_ids_and_names(param1:, param2:) + with_client do |client| + query_params = [param1, param2] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') + client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids_and_names') + end + result = client.exec_prepared('query_sql_get_authors_by_ids_and_names', query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_authors') + client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_truncate_authors') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + unless client.prepared_statements.include?('query_sql_create_book') + client.prepare('query_sql_create_book', CREATE_BOOK_SQL) + client.prepared_statements.add('query_sql_create_book') + end + result = client.exec_prepared('query_sql_create_book', query_params) + result.first['id'] + end + end + + def list_all_authors_books + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_all_authors_books') + client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) + client.prepared_statements.add('query_sql_list_all_authors_books') + end + result = client.exec_prepared('query_sql_list_all_authors_books').values + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def get_duplicate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_duplicate_authors') + client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_get_duplicate_authors') + end + result = client.exec_prepared('query_sql_get_duplicate_authors').values + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') + client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) + client.prepared_statements.add('query_sql_get_authors_by_book_name') + end + result = client.exec_prepared('query_sql_get_authors_by_book_name', query_params).values + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:) + with_client do |client| + query_params = [author_name, name, bio_type] + unless client.prepared_statements.include?('query_sql_create_extended_bio') + client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) + client.prepared_statements.add('query_sql_create_extended_bio') + end + client.exec_prepared('query_sql_create_extended_bio', query_params) + nil + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') + client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + client.prepared_statements.add('query_sql_get_first_extended_bio_by_type') + end + result = client.exec_prepared('query_sql_get_first_extended_bio_by_type', query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + end + end + + def truncate_extended_bios + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extended_bios') + client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) + client.prepared_statements.add('query_sql_truncate_extended_bios') + end + client.exec_prepared('query_sql_truncate_extended_bios') + nil + end + end + + def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) + with_client do |client| + query_params = [ + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ] + unless client.prepared_statements.include?('query_sql_insert_extra_types') + client.prepare('query_sql_insert_extra_types', INSERT_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_insert_extra_types') + end + client.exec_prepared('query_sql_insert_extra_types', query_params) + nil + end + end + + def get_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_extra_types') + client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_get_extra_types') + end + result = client.exec_prepared('query_sql_get_extra_types') + row = result.first + row && GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + end + end + + def truncate_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extra_types') + client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_truncate_extra_types') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('query_sql_many_params') + client.prepare('query_sql_many_params', MANY_PARAMS_SQL) + client.prepared_statements.add('query_sql_many_params') + end + result = client.exec_prepared('query_sql_many_params', query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('query_sql_many_rows') + client.prepare('query_sql_many_rows', MANY_ROWS_SQL) + client.prepared_statements.add('query_sql_many_rows') + end + result = client.exec_prepared('query_sql_many_rows', query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + end + end + + def four_cols + with_client do |client| + unless client.prepared_statements.include?('query_sql_four_cols') + client.prepare('query_sql_four_cols', FOUR_COLS_SQL) + client.prepared_statements.add('query_sql_four_cols') + end + result = client.exec_prepared('query_sql_four_cols') + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs index da08155..ecd1121 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -1,408 +1,301 @@ # auto-generated by sqlc - do not edit module PgCodegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> CreateAuthorRow - end - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - TestSql: String - - class TestRow < ::Struct - attr_accessor c_bit: String? - attr_accessor c_smallint: Integer? - attr_accessor c_boolean: bool? - attr_accessor c_integer: Integer? - attr_accessor c_bigint: Integer? - attr_accessor c_serial: Integer? - attr_accessor c_decimal: BigDecimal? - attr_accessor c_numeric: BigDecimal? - attr_accessor c_real: Float? - attr_accessor c_double_precision: Float? - attr_accessor c_date: Date? - attr_accessor c_time: String? - attr_accessor c_timestamp: Time? - attr_accessor c_char: String? - attr_accessor c_varchar: String? - attr_accessor c_bytea: String? - attr_accessor c_text: String? - attr_accessor c_json: untyped - def self.new: ( - String? c_bit, - Integer? c_smallint, - bool? c_boolean, - Integer? c_integer, - Integer? c_bigint, - Integer? c_serial, - BigDecimal? c_decimal, - BigDecimal? c_numeric, - Float? c_real, - Float? c_double_precision, - Date? c_date, - String? c_time, - Time? c_timestamp, - String? c_char, - String? c_varchar, - String? c_bytea, - String? c_text, - untyped c_json - ) -> TestRow - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow - end - - class ListAuthorsPagedArgs < ::Struct - attr_accessor offset: Integer - attr_accessor limit: Integer - def self.new: ( Integer offset, Integer limit) -> ListAuthorsPagedArgs - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow - end - - class GetAuthorByNamePatternArgs < ::Struct - attr_accessor name_pattern: String? - def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> CreateAuthorReturnIdRow - end - - class CreateAuthorReturnIdArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs < ::Struct - attr_accessor bio: String? - def self.new: ( String? bio) -> UpdateAuthorBiosArgs - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow - end - - class GetAuthorsByIdsArgs < ::Struct - attr_accessor param1: Array[Integer] - def self.new: ( Array[Integer] param1) -> GetAuthorsByIdsArgs - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow - end - - class GetAuthorsByIdsAndNamesArgs < ::Struct - attr_accessor param1: Array[Integer] - attr_accessor param2: Array[String] - def self.new: ( Array[Integer] param1, Array[String] param2) -> GetAuthorsByIdsAndNamesArgs - end - - class CopyAuthorsArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CopyAuthorsArgs - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow < ::Struct - attr_accessor id: String - def self.new: ( String id) -> CreateBookRow - end - - class CreateBookArgs < ::Struct - attr_accessor name: String - attr_accessor author_id: Integer - def self.new: ( String name, Integer author_id) -> CreateBookArgs - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow < ::Struct - attr_accessor author: Author - attr_accessor book: Book - def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow < ::Struct - attr_accessor author: Author - attr_accessor author2: Author - def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - attr_accessor book: Book - def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow - end - - class GetAuthorsByBookNameArgs < ::Struct - attr_accessor name: String - def self.new: ( String name) -> GetAuthorsByBookNameArgs - end - - CreateExtendedBioSql: String - - class CreateExtendedBioArgs < ::Struct - attr_accessor author_name: String - attr_accessor name: String - attr_accessor bio_type: String? - def self.new: ( String author_name, String name, String? bio_type) -> CreateExtendedBioArgs - end - - GetFirstExtendedBioByTypeSql: String - - class GetFirstExtendedBioByTypeRow < ::Struct - attr_accessor author_name: String - attr_accessor name: String - attr_accessor bio_type: String? - def self.new: ( String author_name, String name, String? bio_type) -> GetFirstExtendedBioByTypeRow - end - - class GetFirstExtendedBioByTypeArgs < ::Struct - attr_accessor bio_type: String? - def self.new: ( String? bio_type) -> GetFirstExtendedBioByTypeArgs - end - - TruncateExtendedBiosSql: String - - InsertExtraTypesSql: String - - class InsertExtraTypesArgs < ::Struct - attr_accessor c_uuid: String? - attr_accessor c_jsonb: untyped - attr_accessor c_text_array: Array[String]? - attr_accessor c_integer_array: Array[Integer]? - attr_accessor c_timestamptz: Time? - attr_accessor c_inet: IPAddr? - attr_accessor c_money: String? - attr_accessor c_not_null: String - def self.new: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> InsertExtraTypesArgs - end - - GetExtraTypesSql: String - - class GetExtraTypesRow < ::Struct - attr_accessor c_uuid: String? - attr_accessor c_jsonb: untyped - attr_accessor c_text_array: Array[String]? - attr_accessor c_integer_array: Array[Integer]? - attr_accessor c_timestamptz: Time? - attr_accessor c_inet: IPAddr? - attr_accessor c_money: String? - attr_accessor c_not_null: String - def self.new: ( - String? c_uuid, - untyped c_jsonb, - Array[String]? c_text_array, - Array[Integer]? c_integer_array, - Time? c_timestamptz, - IPAddr? c_inet, - String? c_money, - String c_not_null - ) -> GetExtraTypesRow - end - - TruncateExtraTypesSql: String - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - def self.new: ( bool c0, Integer c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - attr_accessor c4: String - def self.new: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - attr_accessor c4: String - def self.new: ( - bool c0, - Integer c1, - String c2, - Float c3, - String c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: bool - def self.new: ( bool c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: bool - attr_accessor c1: Integer - attr_accessor c2: String - attr_accessor c3: Float - def self.new: ( bool c0, Integer c1, String c2, Float c3) -> FourColsRow - end - - class QuerySql - @db: untyped - @prepared_statements: Hash[untyped, Set[String]] - def initialize: (Hash[String, String], Hash[String, String]) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> CreateAuthorRow? - - def delete_author: (DeleteAuthorArgs) -> void - - def test: -> TestRow? - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> String - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def create_extended_bio: (CreateExtendedBioArgs) -> void - - def get_first_extended_bio_by_type: (GetFirstExtendedBioByTypeArgs) -> GetFirstExtendedBioByTypeRow? - - def truncate_extended_bios: -> void - - def insert_extra_types: (InsertExtraTypesArgs) -> void - - def get_extra_types: -> GetExtraTypesRow? - - def truncate_extra_types: -> void - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CREATE_AUTHOR_SQL: String + + class CreateAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> CreateAuthorRow + end + + DELETE_AUTHOR_SQL: String + + TEST_SQL: String + + class TestRow < ::Struct + attr_accessor c_bit: String? + attr_accessor c_smallint: Integer? + attr_accessor c_boolean: bool? + attr_accessor c_integer: Integer? + attr_accessor c_bigint: Integer? + attr_accessor c_serial: Integer? + attr_accessor c_decimal: BigDecimal? + attr_accessor c_numeric: BigDecimal? + attr_accessor c_real: Float? + attr_accessor c_double_precision: Float? + attr_accessor c_date: Date? + attr_accessor c_time: String? + attr_accessor c_timestamp: Time? + attr_accessor c_char: String? + attr_accessor c_varchar: String? + attr_accessor c_bytea: String? + attr_accessor c_text: String? + attr_accessor c_json: untyped + def self.new: ( + String? c_bit, + Integer? c_smallint, + bool? c_boolean, + Integer? c_integer, + Integer? c_bigint, + Integer? c_serial, + BigDecimal? c_decimal, + BigDecimal? c_numeric, + Float? c_real, + Float? c_double_precision, + Date? c_date, + String? c_time, + Time? c_timestamp, + String? c_char, + String? c_varchar, + String? c_bytea, + String? c_text, + untyped c_json + ) -> TestRow + end + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateAuthorReturnIdRow + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow < ::Struct + attr_accessor id: String + def self.new: ( String id) -> CreateBookRow + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + CREATE_EXTENDED_BIO_SQL: String + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL: String + + class GetFirstExtendedBioByTypeRow < ::Struct + attr_accessor author_name: String + attr_accessor name: String + attr_accessor bio_type: String? + def self.new: ( String author_name, String name, String? bio_type) -> GetFirstExtendedBioByTypeRow + end + + TRUNCATE_EXTENDED_BIOS_SQL: String + + INSERT_EXTRA_TYPES_SQL: String + + GET_EXTRA_TYPES_SQL: String + + class GetExtraTypesRow < ::Struct + attr_accessor c_uuid: String? + attr_accessor c_jsonb: untyped + attr_accessor c_text_array: Array[String]? + attr_accessor c_integer_array: Array[Integer]? + attr_accessor c_timestamptz: Time? + attr_accessor c_inet: IPAddr? + attr_accessor c_money: String? + attr_accessor c_not_null: String + def self.new: ( + String? c_uuid, + untyped c_jsonb, + Array[String]? c_text_array, + Array[Integer]? c_integer_array, + Time? c_timestamptz, + IPAddr? c_inet, + String? c_money, + String c_not_null + ) -> GetExtraTypesRow + end + + TRUNCATE_EXTRA_TYPES_SQL: String + + MANY_PARAMS_SQL: String + + class ManyParamsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + def self.new: ( bool c0, Integer c1) -> ManyParamsRow + end + + MANY_ROWS_SQL: String + + class ManyRowsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + attr_accessor c4: String + def self.new: ( + bool c0, + Integer c1, + String c2, + Float c3, + String c4 + ) -> ManyRowsRow + end + + FOUR_COLS_SQL: String + + class FourColsRow < ::Struct + attr_accessor c0: bool + attr_accessor c1: Integer + attr_accessor c2: String + attr_accessor c3: Float + def self.new: ( bool c0, Integer c1, String c2, Float c3) -> FourColsRow + end + + class QuerySql + def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + + def initialize: (untyped db) -> void + + def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> CreateAuthorRow? + + def delete_author: (id: Integer) -> void + + def test: -> TestRow? + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (param1: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (param1: Array[Integer], param2: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> String + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def create_extended_bio: (author_name: String, name: String, bio_type: String?) -> void + + def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + + def truncate_extended_bios: -> void + + def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void + + def get_extra_types: -> GetExtraTypesRow? + + def truncate_extra_types: -> void + + def many_params: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow? + + def many_rows: (c0: bool) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def with_client: [T] () { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/test/golden/pg/ruby32_notypes/connection.rb b/test/golden/pg/ruby32_notypes/connection.rb new file mode 100644 index 0000000..a6a4492 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/connection.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit + +module PgCodegen + # State kept on every PG::Connection the first time it is used. + module ConnectionState + def prepared_statements + @prepared_statements ||= Set[] + end + end + + ISOLATION_LEVELS = { + read_uncommitted: 'READ UNCOMMITTED', + read_committed: 'READ COMMITTED', + repeatable_read: 'REPEATABLE READ', + serializable: 'SERIALIZABLE' + }.freeze +end diff --git a/test/golden/pg/ruby32_notypes/models.rb b/test/golden/pg/ruby32_notypes/models.rb index 9539cb7..6d0eee8 100644 --- a/test/golden/pg/ruby32_notypes/models.rb +++ b/test/golden/pg/ruby32_notypes/models.rb @@ -1,53 +1,55 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module PgCodegen - module ExtendedBioType - AUTOBIOGRAPHY = "Autobiography" - BIOGRAPHY = "Biography" - MEMOIR = "Memoir" - ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze - end - - class Author < Data.define(:id, :name, :bio) - end - - class NodePostgresType < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end - - class PostgresExtraType < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - class ExtendedBio < Data.define(:author_name, :name, :bio_type) - end -end \ No newline at end of file + module ExtendedBioType + AUTOBIOGRAPHY = 'Autobiography' + BIOGRAPHY = 'Biography' + MEMOIR = 'Memoir' + ALL = [AUTOBIOGRAPHY, BIOGRAPHY, MEMOIR].freeze + end + + class Author < Data.define(:id, :name, :bio) + end + + class NodePostgresType < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end + + class PostgresExtraType < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + class ExtendedBio < Data.define(:author_name, :name, :bio_type) + end +end diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb index 5cb8646..b56f64e 100644 --- a/test/golden/pg/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -1,47 +1,78 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def with_numbers2_go(with_numbers2_go_args) - @db.with do |client| - query_params = [with_numbers2_go_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('with_numbers2_go') - client.prepare('with_numbers2_go', WithNumbers2GoSql) - prepared.add('with_numbers2_go') - end - client.exec_prepared('with_numbers2_go', query_params) - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def with_numbers2_go(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') + client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) + client.prepared_statements.add('my_queries_sql_with_numbers2_go') + end + client.exec_prepared('my_queries_sql_with_numbers2_go', query_params) + nil + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb index 59b7056..1a6fb1f 100644 --- a/test/golden/pg/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -1,99 +1,121 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def no_cols(no_cols_args) - @db.with do |client| - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('no_cols') - client.prepare('no_cols', NoColsSql) - prepared.add('no_cols') - end - client.exec_prepared('no_cols', query_params) - end - end - - def other_one - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('other_one') - client.prepare('other_one', OtherOneSql) - prepared.add('other_one') - end - result = client.exec_prepared('other_one') - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row['c0']) - return entity - end - end - - def count_rows(count_rows_args) - @db.with do |client| - query_params = [count_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('count_rows') - client.prepare('count_rows', CountRowsSql) - prepared.add('count_rows') - end - result = client.exec_prepared('count_rows', query_params) - return result.cmd_tuples - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('other_sql_no_cols') + client.prepare('other_sql_no_cols', NO_COLS_SQL) + client.prepared_statements.add('other_sql_no_cols') + end + client.exec_prepared('other_sql_no_cols', query_params) + nil + end + end + + def other_one + with_client do |client| + unless client.prepared_statements.include?('other_sql_other_one') + client.prepare('other_sql_other_one', OTHER_ONE_SQL) + client.prepared_statements.add('other_sql_other_one') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + end + end + + def count_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('other_sql_count_rows') + client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) + client.prepared_statements.add('other_sql_count_rows') + end + result = client.exec_prepared('other_sql_count_rows', query_params) + result.cmd_tuples + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb index 60af7b6..aa15016 100644 --- a/test/golden/pg/ruby32_notypes/query_sql.rb +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -1,696 +1,668 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'pg' -require 'set' +require_relative 'connection' require_relative 'models' module PgCodegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = $1 LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - $1, $2 - ) - RETURNING id, name, bio) - - class CreateAuthorRow < Data.define(:id, :name, :bio) - end - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = $1) - - class DeleteAuthorArgs < Data.define(:id) - end - - TestSql = %q(SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types - LIMIT 1) - - class TestRow < Data.define( - :c_bit, - :c_smallint, - :c_boolean, - :c_integer, - :c_bigint, - :c_serial, - :c_decimal, - :c_numeric, - :c_real, - :c_double_precision, - :c_date, - :c_time, - :c_timestamp, - :c_char, - :c_varchar, - :c_bytea, - :c_text, - :c_json - ) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT $2 - OFFSET $1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE($1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = $1 - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT [])) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:param1) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors - WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT [])) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:param1, :param2) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(TRUNCATE TABLE authors CASCADE) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors - INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = $1) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - CreateExtendedBioSql = %q(INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3)) - - class CreateExtendedBioArgs < Data.define(:author_name, :name, :bio_type) - end - - GetFirstExtendedBioByTypeSql = %q(SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1) - - class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) - end - - class GetFirstExtendedBioByTypeArgs < Data.define(:bio_type) - end - - TruncateExtendedBiosSql = %q(TRUNCATE TABLE extended.bios) - - InsertExtraTypesSql = %q(INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)) - - class InsertExtraTypesArgs < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - GetExtraTypesSql = %q(SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1) - - class GetExtraTypesRow < Data.define( - :c_uuid, - :c_jsonb, - :c_text_array, - :c_integer_array, - :c_timestamptz, - :c_inet, - :c_money, - :c_not_null - ) - end - - TruncateExtraTypesSql = %q(TRUNCATE TABLE postgres_extra_types) - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(connection_pool_params, pg_params) - @db = ConnectionPool.new(**connection_pool_params) { - client = PG.connect(**pg_params) - type_map = PG::BasicTypeMapForResults.new client - type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings - client.type_map_for_results = type_map - client.type_map_for_queries = PG::BasicTypeMapForQueries.new client - client - } - @prepared_statements = {}.compare_by_identity - end - - def transaction - @db.with do |client| - client.transaction do - yield self - end - end - end - - def get_author(get_author_args) - @db.with do |client| - query_params = [get_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author') - client.prepare('get_author', GetAuthorSql) - prepared.add('get_author') - end - result = client.exec_prepared('get_author', query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def list_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors') - client.prepare('list_authors', ListAuthorsSql) - prepared.add('list_authors') - end - result = client.exec_prepared('list_authors') - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author(create_author_args) - @db.with do |client| - query_params = [create_author_args.name, create_author_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author') - client.prepare('create_author', CreateAuthorSql) - prepared.add('create_author') - end - result = client.exec_prepared('create_author', query_params) - row = result.first - return nil if row.nil? - entity = CreateAuthorRow.new(row['id'], row['name'], row['bio']) - return entity - end - end - - def delete_author(delete_author_args) - @db.with do |client| - query_params = [delete_author_args.id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('delete_author') - client.prepare('delete_author', DeleteAuthorSql) - prepared.add('delete_author') - end - client.exec_prepared('delete_author', query_params) - end - end - - def test - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('test') - client.prepare('test', TestSql) - prepared.add('test') - end - result = client.exec_prepared('test') - row = result.first - return nil if row.nil? - entity = TestRow.new( - row['c_bit'], - row['c_smallint'], - row['c_boolean'], - row['c_integer'], - row['c_bigint'], - row['c_serial'], - row['c_decimal'], - row['c_numeric'], - row['c_real'], - row['c_double_precision'], - row['c_date'], - row['c_time'], - row['c_timestamp'], - row['c_char'], - row['c_varchar'], - row['c_bytea'], - row['c_text'], - row['c_json'] - ) - return entity - end - end - - def list_authors_paged(list_authors_paged_args) - @db.with do |client| - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_authors_paged') - client.prepare('list_authors_paged', ListAuthorsPagedSql) - prepared.add('list_authors_paged') - end - result = client.exec_prepared('list_authors_paged', query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - @db.with do |client| - query_params = [get_author_by_name_pattern_args.name_pattern] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_author_by_name_pattern') - client.prepare('get_author_by_name_pattern', GetAuthorByNamePatternSql) - prepared.add('get_author_by_name_pattern') - end - result = client.exec_prepared('get_author_by_name_pattern', query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def create_author_return_id(create_author_return_id_args) - @db.with do |client| - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_author_return_id') - client.prepare('create_author_return_id', CreateAuthorReturnIdSql) - prepared.add('create_author_return_id') - end - result = client.exec_prepared('create_author_return_id', query_params) - return result.first['id'] - end - end - - def update_author_bios(update_author_bios_args) - @db.with do |client| - query_params = [update_author_bios_args.bio] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('update_author_bios') - client.prepare('update_author_bios', UpdateAuthorBiosSql) - prepared.add('update_author_bios') - end - result = client.exec_prepared('update_author_bios', query_params) - return result.cmd_tuples - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - @db.with do |client| - query_params = [get_authors_by_ids_args.param1] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids') - client.prepare('get_authors_by_ids', GetAuthorsByIdsSql) - prepared.add('get_authors_by_ids') - end - result = client.exec_prepared('get_authors_by_ids', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - @db.with do |client| - query_params = [get_authors_by_ids_and_names_args.param1, get_authors_by_ids_and_names_args.param2] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_ids_and_names') - client.prepare('get_authors_by_ids_and_names', GetAuthorsByIdsAndNamesSql) - prepared.add('get_authors_by_ids_and_names') - end - result = client.exec_prepared('get_authors_by_ids_and_names', query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) - end - return entities - end - end - - def copy_authors(copy_authors_args) - @db.with do |client| - encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) - client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do - copy_authors_args.each do |item| - client.put_copy_data([item.name, item.bio]) - end - end - end - end - - def truncate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_authors') - client.prepare('truncate_authors', TruncateAuthorsSql) - prepared.add('truncate_authors') - end - client.exec_prepared('truncate_authors') - end - end - - def create_book(create_book_args) - @db.with do |client| - query_params = [create_book_args.name, create_book_args.author_id] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_book') - client.prepare('create_book', CreateBookSql) - prepared.add('create_book') - end - result = client.exec_prepared('create_book', query_params) - return result.first['id'] - end - end - - def list_all_authors_books - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('list_all_authors_books') - client.prepare('list_all_authors_books', ListAllAuthorsBooksSql) - prepared.add('list_all_authors_books') - end - result = client.exec_prepared('list_all_authors_books').values - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def get_duplicate_authors - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_duplicate_authors') - client.prepare('get_duplicate_authors', GetDuplicateAuthorsSql) - prepared.add('get_duplicate_authors') - end - result = client.exec_prepared('get_duplicate_authors').values - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - @db.with do |client| - query_params = [get_authors_by_book_name_args.name] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_authors_by_book_name') - client.prepare('get_authors_by_book_name', GetAuthorsByBookNameSql) - prepared.add('get_authors_by_book_name') - end - result = client.exec_prepared('get_authors_by_book_name', query_params).values - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - end - end - - def create_extended_bio(create_extended_bio_args) - @db.with do |client| - query_params = [create_extended_bio_args.author_name, create_extended_bio_args.name, create_extended_bio_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('create_extended_bio') - client.prepare('create_extended_bio', CreateExtendedBioSql) - prepared.add('create_extended_bio') - end - client.exec_prepared('create_extended_bio', query_params) - end - end - - def get_first_extended_bio_by_type(get_first_extended_bio_by_type_args) - @db.with do |client| - query_params = [get_first_extended_bio_by_type_args.bio_type] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_first_extended_bio_by_type') - client.prepare('get_first_extended_bio_by_type', GetFirstExtendedBioByTypeSql) - prepared.add('get_first_extended_bio_by_type') - end - result = client.exec_prepared('get_first_extended_bio_by_type', query_params) - row = result.first - return nil if row.nil? - entity = GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) - return entity - end - end - - def truncate_extended_bios - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extended_bios') - client.prepare('truncate_extended_bios', TruncateExtendedBiosSql) - prepared.add('truncate_extended_bios') - end - client.exec_prepared('truncate_extended_bios') - end - end - - def insert_extra_types(insert_extra_types_args) - @db.with do |client| - query_params = [ - insert_extra_types_args.c_uuid, - insert_extra_types_args.c_jsonb, - insert_extra_types_args.c_text_array, - insert_extra_types_args.c_integer_array, - insert_extra_types_args.c_timestamptz, - insert_extra_types_args.c_inet, - insert_extra_types_args.c_money, - insert_extra_types_args.c_not_null - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('insert_extra_types') - client.prepare('insert_extra_types', InsertExtraTypesSql) - prepared.add('insert_extra_types') - end - client.exec_prepared('insert_extra_types', query_params) - end - end - - def get_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('get_extra_types') - client.prepare('get_extra_types', GetExtraTypesSql) - prepared.add('get_extra_types') - end - result = client.exec_prepared('get_extra_types') - row = result.first - return nil if row.nil? - entity = GetExtraTypesRow.new( - row['c_uuid'], - row['c_jsonb'], - row['c_text_array'], - row['c_integer_array'], - row['c_timestamptz'], - row['c_inet'], - row['c_money'], - row['c_not_null'] - ) - return entity - end - end - - def truncate_extra_types - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('truncate_extra_types') - client.prepare('truncate_extra_types', TruncateExtraTypesSql) - prepared.add('truncate_extra_types') - end - client.exec_prepared('truncate_extra_types') - end - end - - def many_params(many_params_args) - @db.with do |client| - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_params') - client.prepare('many_params', ManyParamsSql) - prepared.add('many_params') - end - result = client.exec_prepared('many_params', query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row['c0'], row['c1']) - return entity - end - end - - def many_rows(many_rows_args) - @db.with do |client| - query_params = [many_rows_args.c0] - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('many_rows') - client.prepare('many_rows', manyRowsSql) - prepared.add('many_rows') - end - result = client.exec_prepared('many_rows', query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row['c0'], - row['c1'], - row['c2'], - row['c3'], - row['c4'] - ) - end - return entities - end - end - - def four_cols - @db.with do |client| - prepared = (@prepared_statements[client] ||= Set[]) - unless prepared.include?('four_cols') - client.prepare('four_cols', FourColsSql) - prepared.add('four_cols') - end - result = client.exec_prepared('four_cols') - entities = [] - result.each do |row| - entities << FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) - end - return entities - end - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + $1, $2 + ) + RETURNING id, name, bio + SQL + + class CreateAuthorRow < Data.define(:id, :name, :bio) + end + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = $1 + SQL + + TEST_SQL = <<~'SQL' + SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types + LIMIT 1 + SQL + + class TestRow < Data.define( + :c_bit, + :c_smallint, + :c_boolean, + :c_integer, + :c_bigint, + :c_serial, + :c_decimal, + :c_numeric, + :c_real, + :c_double_precision, + :c_date, + :c_time, + :c_timestamp, + :c_char, + :c_varchar, + :c_bytea, + :c_text, + :c_json + ) + end + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT $2 + OFFSET $1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE($1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = $1 + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ANY($1::BIGINT []) AND name = ANY($2::TEXT []) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + TRUNCATE TABLE authors CASCADE + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES ($1, $2) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors + INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = $1 + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + CREATE_EXTENDED_BIO_SQL = <<~'SQL' + INSERT INTO extended.bios (author_name, name, bio_type) VALUES ($1, $2, $3) + SQL + + GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL = <<~'SQL' + SELECT author_name, name, bio_type FROM extended.bios WHERE bio_type = $1 LIMIT 1 + SQL + + class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type) + end + + TRUNCATE_EXTENDED_BIOS_SQL = <<~'SQL' + TRUNCATE TABLE extended.bios + SQL + + INSERT_EXTRA_TYPES_SQL = <<~'SQL' + INSERT INTO postgres_extra_types (c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + SQL + + GET_EXTRA_TYPES_SQL = <<~'SQL' + SELECT c_uuid, c_jsonb, c_text_array, c_integer_array, c_timestamptz, c_inet, c_money, c_not_null FROM postgres_extra_types LIMIT 1 + SQL + + class GetExtraTypesRow < Data.define( + :c_uuid, + :c_jsonb, + :c_text_array, + :c_integer_array, + :c_timestamptz, + :c_inet, + :c_money, + :c_not_null + ) + end + + TRUNCATE_EXTRA_TYPES_SQL = <<~'SQL' + TRUNCATE TABLE postgres_extra_types + SQL + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + end + + def initialize(db) + @db = db + end + + # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, + # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. + def transaction(isolation: nil) + with_client do |client| + outermost = client.transaction_status == PG::PQTRANS_IDLE + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + if outermost + client.exec(isolation ? "BEGIN ISOLATION LEVEL #{ISOLATION_LEVELS.fetch(isolation)}" : 'BEGIN') + else + client.exec("SAVEPOINT #{savepoint}") + end + begin + result = yield self + client.exec(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + client.exec(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_get_author') + client.prepare('query_sql_get_author', GET_AUTHOR_SQL) + client.prepared_statements.add('query_sql_get_author') + end + result = client.exec_prepared('query_sql_get_author', query_params) + row = result.first + row && GetAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def list_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_authors') + client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) + client.prepared_statements.add('query_sql_list_authors') + end + result = client.exec_prepared('query_sql_list_authors') + result.map do |row| + ListAuthorsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author') + client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_create_author') + end + result = client.exec_prepared('query_sql_create_author', query_params) + row = result.first + row && CreateAuthorRow.new(row['id'], row['name'], row['bio']) + end + end + + def delete_author(id:) + with_client do |client| + query_params = [id] + unless client.prepared_statements.include?('query_sql_delete_author') + client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) + client.prepared_statements.add('query_sql_delete_author') + end + client.exec_prepared('query_sql_delete_author', query_params) + nil + end + end + + def test + with_client do |client| + unless client.prepared_statements.include?('query_sql_test') + client.prepare('query_sql_test', TEST_SQL) + client.prepared_statements.add('query_sql_test') + end + result = client.exec_prepared('query_sql_test') + row = result.first + row && TestRow.new( + row['c_bit'], + row['c_smallint'], + row['c_boolean'], + row['c_integer'], + row['c_bigint'], + row['c_serial'], + row['c_decimal'], + row['c_numeric'], + row['c_real'], + row['c_double_precision'], + row['c_date'], + row['c_time'], + row['c_timestamp'], + row['c_char'], + row['c_varchar'], + row['c_bytea'], + row['c_text'], + row['c_json'] + ) + end + end + + def list_authors_paged(offset:, limit:) + with_client do |client| + query_params = [offset, limit] + unless client.prepared_statements.include?('query_sql_list_authors_paged') + client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) + client.prepared_statements.add('query_sql_list_authors_paged') + end + result = client.exec_prepared('query_sql_list_authors_paged', query_params) + result.map do |row| + ListAuthorsPagedRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_author_by_name_pattern(name_pattern: nil) + with_client do |client| + query_params = [name_pattern] + unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') + client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) + client.prepared_statements.add('query_sql_get_author_by_name_pattern') + end + result = client.exec_prepared('query_sql_get_author_by_name_pattern', query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def create_author_return_id(name:, bio:) + with_client do |client| + query_params = [name, bio] + unless client.prepared_statements.include?('query_sql_create_author_return_id') + client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) + client.prepared_statements.add('query_sql_create_author_return_id') + end + result = client.exec_prepared('query_sql_create_author_return_id', query_params) + result.first['id'] + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + with_client do |client| + query_params = [bio] + unless client.prepared_statements.include?('query_sql_update_author_bios') + client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) + client.prepared_statements.add('query_sql_update_author_bios') + end + result = client.exec_prepared('query_sql_update_author_bios', query_params) + result.cmd_tuples + end + end + + def get_authors_by_ids(param1:) + with_client do |client| + query_params = [param1] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids') + client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids') + end + result = client.exec_prepared('query_sql_get_authors_by_ids', query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row['id'], row['name'], row['bio']) + end + end + end + + def get_authors_by_ids_and_names(param1:, param2:) + with_client do |client| + query_params = [param1, param2] + unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') + client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) + client.prepared_statements.add('query_sql_get_authors_by_ids_and_names') + end + result = client.exec_prepared('query_sql_get_authors_by_ids_and_names', query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row['id'], row['name'], row['bio']) + end + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + with_client do |client| + encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_authors') + client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_truncate_authors') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + with_client do |client| + query_params = [name, author_id] + unless client.prepared_statements.include?('query_sql_create_book') + client.prepare('query_sql_create_book', CREATE_BOOK_SQL) + client.prepared_statements.add('query_sql_create_book') + end + result = client.exec_prepared('query_sql_create_book', query_params) + result.first['id'] + end + end + + def list_all_authors_books + with_client do |client| + unless client.prepared_statements.include?('query_sql_list_all_authors_books') + client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) + client.prepared_statements.add('query_sql_list_all_authors_books') + end + result = client.exec_prepared('query_sql_list_all_authors_books').values + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def get_duplicate_authors + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_duplicate_authors') + client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) + client.prepared_statements.add('query_sql_get_duplicate_authors') + end + result = client.exec_prepared('query_sql_get_duplicate_authors').values + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + end + end + + def get_authors_by_book_name(name:) + with_client do |client| + query_params = [name] + unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') + client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) + client.prepared_statements.add('query_sql_get_authors_by_book_name') + end + result = client.exec_prepared('query_sql_get_authors_by_book_name', query_params).values + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + end + end + + def create_extended_bio(author_name:, name:, bio_type:) + with_client do |client| + query_params = [author_name, name, bio_type] + unless client.prepared_statements.include?('query_sql_create_extended_bio') + client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) + client.prepared_statements.add('query_sql_create_extended_bio') + end + client.exec_prepared('query_sql_create_extended_bio', query_params) + nil + end + end + + def get_first_extended_bio_by_type(bio_type:) + with_client do |client| + query_params = [bio_type] + unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') + client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) + client.prepared_statements.add('query_sql_get_first_extended_bio_by_type') + end + result = client.exec_prepared('query_sql_get_first_extended_bio_by_type', query_params) + row = result.first + row && GetFirstExtendedBioByTypeRow.new(row['author_name'], row['name'], row['bio_type']) + end + end + + def truncate_extended_bios + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extended_bios') + client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) + client.prepared_statements.add('query_sql_truncate_extended_bios') + end + client.exec_prepared('query_sql_truncate_extended_bios') + nil + end + end + + def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) + with_client do |client| + query_params = [ + c_uuid, + c_jsonb, + c_text_array, + c_integer_array, + c_timestamptz, + c_inet, + c_money, + c_not_null + ] + unless client.prepared_statements.include?('query_sql_insert_extra_types') + client.prepare('query_sql_insert_extra_types', INSERT_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_insert_extra_types') + end + client.exec_prepared('query_sql_insert_extra_types', query_params) + nil + end + end + + def get_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_get_extra_types') + client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_get_extra_types') + end + result = client.exec_prepared('query_sql_get_extra_types') + row = result.first + row && GetExtraTypesRow.new( + row['c_uuid'], + row['c_jsonb'], + row['c_text_array'], + row['c_integer_array'], + row['c_timestamptz'], + row['c_inet'], + row['c_money'], + row['c_not_null'] + ) + end + end + + def truncate_extra_types + with_client do |client| + unless client.prepared_statements.include?('query_sql_truncate_extra_types') + client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) + client.prepared_statements.add('query_sql_truncate_extra_types') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + unless client.prepared_statements.include?('query_sql_many_params') + client.prepare('query_sql_many_params', MANY_PARAMS_SQL) + client.prepared_statements.add('query_sql_many_params') + end + result = client.exec_prepared('query_sql_many_params', query_params) + row = result.first + row && ManyParamsRow.new(row['c0'], row['c1']) + end + end + + def many_rows(c0:) + with_client do |client| + query_params = [c0] + unless client.prepared_statements.include?('query_sql_many_rows') + client.prepare('query_sql_many_rows', MANY_ROWS_SQL) + client.prepared_statements.add('query_sql_many_rows') + end + result = client.exec_prepared('query_sql_many_rows', query_params) + result.map do |row| + manyRowsRow.new( + row['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + end + end + + def four_cols + with_client do |client| + unless client.prepared_statements.include?('query_sql_four_cols') + client.prepare('query_sql_four_cols', FOUR_COLS_SQL) + client.prepared_statements.add('query_sql_four_cols') + end + result = client.exec_prepared('query_sql_four_cols') + result.map do |row| + FourColsRow.new(row['c0'], row['c1'], row['c2'], row['c3']) + end + end + end + + private + + def with_client + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + end + + # Connections of an injected pool are set up on first use: typed results and Ruby arrays as parameters. + def configure(client) + type_map = PG::BasicTypeMapForResults.new(client) + type_map.default_type_map = PG::TypeMapAllStrings.new # uuid, enums, money, ... come back as Strings + client.type_map_for_results = type_map + client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client) + client.extend(ConnectionState) + end + end +end diff --git a/test/golden/sqlite3/default/models.rb b/test/golden/sqlite3/default/models.rb index c96b91e..bf6966f 100644 --- a/test/golden/sqlite3/default/models.rb +++ b/test/golden/sqlite3/default/models.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author < Data.define(:id, :name, :bio) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end -end \ No newline at end of file + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end diff --git a/test/golden/sqlite3/default/models.rbs b/test/golden/sqlite3/default/models.rbs index df9c5ba..cd9946a 100644 --- a/test/golden/sqlite3/default/models.rbs +++ b/test/golden/sqlite3/default/models.rbs @@ -1,17 +1,17 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class Book - attr_reader id: Integer - attr_reader name: String - attr_reader author_id: Integer - attr_reader description: String? - def initialize: ( Integer id, String name, Integer author_id, String? description) -> void - end -end \ No newline at end of file + class Author + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class Book + attr_reader id: Integer + attr_reader name: String + attr_reader author_id: Integer + attr_reader description: String? + def initialize: ( Integer id, String name, Integer author_id, String? description) -> void + end +end diff --git a/test/golden/sqlite3/default/my_queries_sql.rb b/test/golden/sqlite3/default/my_queries_sql.rb index b6a7fc5..6ad5713 100644 --- a/test/golden/sqlite3/default/my_queries_sql.rb +++ b/test/golden/sqlite3/default/my_queries_sql.rb @@ -1,35 +1,48 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def with_numbers2_go(with_numbers2_go_args) - client = @db - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def with_numbers2_go(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/default/my_queries_sql.rbs b/test/golden/sqlite3/default/my_queries_sql.rbs index 2c1d2d2..52b5923 100644 --- a/test/golden/sqlite3/default/my_queries_sql.rbs +++ b/test/golden/sqlite3/default/my_queries_sql.rbs @@ -1,18 +1,12 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - class My_queriesSql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: Integer) -> void + end +end diff --git a/test/golden/sqlite3/default/other_sql.rb b/test/golden/sqlite3/default/other_sql.rb index bbdfe6f..48bd104 100644 --- a/test/golden/sqlite3/default/other_sql.rb +++ b/test/golden/sqlite3/default/other_sql.rb @@ -1,85 +1,91 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def no_cols(no_cols_args) - client = @db - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def other_one - client = @db - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity - ensure - stmt.close - end - end - - def count_rows(count_rows_args) - client = @db - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def other_one + client = @db + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end + end + + def count_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/default/other_sql.rbs b/test/golden/sqlite3/default/other_sql.rbs index a9ea16b..2eac5c8 100644 --- a/test/golden/sqlite3/default/other_sql.rbs +++ b/test/golden/sqlite3/default/other_sql.rbs @@ -1,46 +1,25 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - NoColsSql: String - - class NoColsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - OtherOneSql: String - - class OtherOneRow - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - CountRowsSql: String - - class CountRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - class OtherSql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow + attr_reader c0: Integer + def initialize: ( Integer c0) -> void + end + + COUNT_ROWS_SQL: String + + class OtherSql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: Integer) -> Integer + end +end diff --git a/test/golden/sqlite3/default/query_sql.rb b/test/golden/sqlite3/default/query_sql.rb index 3d09e88..8cfab6a 100644 --- a/test/golden/sqlite3/default/query_sql.rb +++ b/test/golden/sqlite3/default/query_sql.rb @@ -1,462 +1,450 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ?2 OFFSET ?1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - ensure - stmt.close - end - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def list_authors_paged(list_authors_paged_args) - client = @db - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - client = @db - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author_return_id(create_author_return_id_args) - client = @db - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def update_author_bios(update_author_bios_args) - client = @db - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - client = @db - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - client = @db - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def copy_authors(copy_authors_args) - client = @db - copy_authors_args.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) - end - end - - def truncate_authors - client = @db - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - - def create_book(create_book_args) - client = @db - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def list_all_authors_books - client = @db - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def get_duplicate_authors - client = @db - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - client = @db - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - ensure - stmt.close - end - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - ensure - stmt.close - end - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - ensure - stmt.close - end - end - - private - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def delete_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def list_authors_paged(offset:, limit:) + client = @db + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_author_by_name_pattern(name_pattern: nil) + client = @db + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author_return_id(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + client = @db + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(ids:) + client = @db + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(ids:, names:) + client = @db + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + client = @db + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil + end + + def truncate_authors + client = @db + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + + def create_book(name:, author_id:) + client = @db + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + + def get_authors_by_book_name(name:) + client = @db + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end + end + + def many_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs index 30d55b4..d7de3f0 100644 --- a/test/golden/sqlite3/default/query_sql.rbs +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -1,273 +1,193 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - GetAuthorSql: String - - class GetAuthorRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsSql: String - - class ListAuthorsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - CreateAuthorSql: String - - class CreateAuthorArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class ListAuthorsPagedArgs - attr_reader offset: Integer - attr_reader limit: Integer - def initialize: ( Integer offset, Integer limit) -> void - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorByNamePatternArgs - attr_reader name_pattern: String? - def initialize: ( String? name_pattern) -> void - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateAuthorReturnIdArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs - attr_reader bio: String? - def initialize: ( String? bio) -> void - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsArgs - attr_reader ids: Array[Integer] - def initialize: ( Array[Integer] ids) -> void - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - def initialize: ( Integer id, String name, String? bio) -> void - end - - class GetAuthorsByIdsAndNamesArgs - attr_reader ids: Array[Integer] - attr_reader names: Array[String] - def initialize: ( Array[Integer] ids, Array[String] names) -> void - end - - class CopyAuthorsArgs - attr_reader name: String - attr_reader bio: String? - def initialize: ( String name, String? bio) -> void - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow - attr_reader id: Integer - def initialize: ( Integer id) -> void - end - - class CreateBookArgs - attr_reader name: String - attr_reader author_id: Integer - def initialize: ( String name, Integer author_id) -> void - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow - attr_reader author: Author - attr_reader book: Book - def initialize: ( Author author, Book book) -> void - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow - attr_reader author: Author - attr_reader author2: Author - def initialize: ( Author author, Author author2) -> void - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow - attr_reader id: Integer - attr_reader name: String - attr_reader bio: String? - attr_reader book: Book - def initialize: ( Integer id, String name, String? bio, Book book) -> void - end - - class GetAuthorsByBookNameArgs - attr_reader name: String - def initialize: ( String name) -> void - end - - ManyParamsSql: String - - class ManyParamsRow - attr_reader c0: Integer - attr_reader c1: String - def initialize: ( Integer c0, String c1) -> void - end - - class ManyParamsArgs - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - manyRowsSql: String - - class ManyRowsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - attr_reader c4: Integer - def initialize: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> void - end - - class ManyRowsArgs - attr_reader c0: Integer - def initialize: ( Integer c0) -> void - end - - FourColsSql: String - - class FourColsRow - attr_reader c0: Integer - attr_reader c1: String - attr_reader c2: Float - attr_reader c3: String - def initialize: ( Integer c0, String c1, Float c2, String c3) -> void - end - - class QuerySql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def delete_author: (DeleteAuthorArgs) -> void - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - - private - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_SQL: String + + DELETE_AUTHOR_SQL: String + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + def initialize: ( Integer id, String name, String? bio) -> void + end + + class CopyAuthorsArgs + attr_reader name: String + attr_reader bio: String? + def initialize: ( String name, String? bio) -> void + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow + attr_reader id: Integer + def initialize: ( Integer id) -> void + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow + attr_reader author: Author + attr_reader book: Book + def initialize: ( Author author, Book book) -> void + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow + attr_reader author: Author + attr_reader author2: Author + def initialize: ( Author author, Author author2) -> void + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow + attr_reader id: Integer + attr_reader name: String + attr_reader bio: String? + attr_reader book: Book + def initialize: ( Integer id, String name, String? bio, Book book) -> void + end + + MANY_PARAMS_SQL: String + + class ManyParamsRow + attr_reader c0: Integer + attr_reader c1: String + def initialize: ( Integer c0, String c1) -> void + end + + MANY_ROWS_SQL: String + + class ManyRowsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: String + attr_reader c4: Integer + def initialize: ( + Integer c0, + String c1, + Float c2, + String c3, + Integer c4 + ) -> void + end + + FOUR_COLS_SQL: String + + class FourColsRow + attr_reader c0: Integer + attr_reader c1: String + attr_reader c2: Float + attr_reader c3: String + def initialize: ( Integer c0, String c1, Float c2, String c3) -> void + end + + class QuerySql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def delete_author: (id: Integer) -> void + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + + def many_rows: (c0: Integer) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rb b/test/golden/sqlite3/ruby31_gemfile/models.rb index 7d247be..f5eca1d 100644 --- a/test/golden/sqlite3/ruby31_gemfile/models.rb +++ b/test/golden/sqlite3/ruby31_gemfile/models.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author < Struct.new(id, name, bio) - end - - class Book < Struct.new(id, name, author_id, description) - end -end \ No newline at end of file + class Author < Struct.new(:id, :name, :bio) + end + + class Book < Struct.new(:id, :name, :author_id, :description) + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rbs b/test/golden/sqlite3/ruby31_gemfile/models.rbs index 3c6a328..857a90e 100644 --- a/test/golden/sqlite3/ruby31_gemfile/models.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/models.rbs @@ -1,17 +1,17 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> Author - end - - class Book < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor author_id: Integer - attr_accessor description: String? - def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book - end -end \ No newline at end of file + class Author < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> Author + end + + class Book < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor author_id: Integer + attr_accessor description: String? + def self.new: ( Integer id, String name, Integer author_id, String? description) -> Book + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb index a4796b1..6ad5713 100644 --- a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb @@ -1,35 +1,48 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Struct.new(c0) - end - - class My_queriesSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def with_numbers2_go(with_numbers2_go_args) - client = @db - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def with_numbers2_go(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs index 1637bf8..52b5923 100644 --- a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs @@ -1,18 +1,12 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - WithNumbers2GoSql: String - - class WithNumbers2GoArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> WithNumbers2GoArgs - end - - class My_queriesSql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def with_numbers2_go: (WithNumbers2GoArgs) -> void - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def with_numbers2_go: (c0: Integer) -> void + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rb b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb index af3912a..ba3ce95 100644 --- a/test/golden/sqlite3/ruby31_gemfile/other_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb @@ -1,85 +1,91 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Struct.new(c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Struct.new(c0) - end - - class OtherSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def no_cols(no_cols_args) - client = @db - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def other_one - client = @db - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity - ensure - stmt.close - end - end - - def count_rows(count_rows_args) - client = @db - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Struct.new(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def other_one + client = @db + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end + end + + def count_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs index 1f61cf2..271a9aa 100644 --- a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs @@ -1,46 +1,25 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - NoColsSql: String - - class NoColsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> NoColsArgs - end - - OtherOneSql: String - - class OtherOneRow < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> OtherOneRow - end - - CountRowsSql: String - - class CountRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> CountRowsArgs - end - - class OtherSql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def no_cols: (NoColsArgs) -> void - - def other_one: -> OtherOneRow? - - def count_rows: (CountRowsArgs) -> Integer - end -end \ No newline at end of file + NO_COLS_SQL: String + + OTHER_ONE_SQL: String + + class OtherOneRow < ::Struct + attr_accessor c0: Integer + def self.new: ( Integer c0) -> OtherOneRow + end + + COUNT_ROWS_SQL: String + + class OtherSql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + def other_one: -> OtherOneRow? + + def count_rows: (c0: Integer) -> Integer + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb index 7b21662..627d509 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -1,462 +1,450 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Struct.new(id, name, bio) - end - - class GetAuthorArgs < Struct.new(id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Struct.new(id, name, bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Struct.new(name, bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Struct.new(id) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ?2 OFFSET ?1) - - class ListAuthorsPagedRow < Struct.new(id, name, bio) - end - - class ListAuthorsPagedArgs < Struct.new(offset, limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?1, '%')) - - class GetAuthorByNamePatternRow < Struct.new(id, name, bio) - end - - class GetAuthorByNamePatternArgs < Struct.new(name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) - - class CreateAuthorReturnIdRow < Struct.new(id) - end - - class CreateAuthorReturnIdArgs < Struct.new(name, bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Struct.new(bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsArgs < Struct.new(ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Struct.new(id, name, bio) - end - - class GetAuthorsByIdsAndNamesArgs < Struct.new(ids, names) - end - - class CopyAuthorsArgs < Struct.new(name, bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) - - class CreateBookRow < Struct.new(id) - end - - class CreateBookArgs < Struct.new(name, author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Struct.new(author, book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Struct.new(author, author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Struct.new(id, name, bio, book) - end - - class GetAuthorsByBookNameArgs < Struct.new(name) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Struct.new(c0, c1) - end - - class ManyParamsArgs < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Struct.new( - c0, - c1, - c2, - c3, - c4 - ) - end - - class ManyRowsArgs < Struct.new(c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Struct.new(c0, c1, c2, c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - ensure - stmt.close - end - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def list_authors_paged(list_authors_paged_args) - client = @db - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - client = @db - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author_return_id(create_author_return_id_args) - client = @db - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def update_author_bios(update_author_bios_args) - client = @db - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - client = @db - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - client = @db - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def copy_authors(copy_authors_args) - client = @db - copy_authors_args.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) - end - end - - def truncate_authors - client = @db - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - - def create_book(create_book_args) - client = @db - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def list_all_authors_books - client = @db - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def get_duplicate_authors - client = @db - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - client = @db - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - ensure - stmt.close - end - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - ensure - stmt.close - end - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - ensure - stmt.close - end - end - - private - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Struct.new(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Struct.new(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1 + SQL + + class ListAuthorsPagedRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%') + SQL + + class GetAuthorByNamePatternRow < Struct.new(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Struct.new(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Struct.new(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Struct.new(:id, :name, :bio) + end + + class CopyAuthorsArgs < Struct.new(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id + SQL + + class CreateBookRow < Struct.new(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Struct.new(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Struct.new(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Struct.new(:id, :name, :bio, :book) + end + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Struct.new(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Struct.new( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) + end + + class QuerySql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def delete_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def list_authors_paged(offset:, limit:) + client = @db + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_author_by_name_pattern(name_pattern: nil) + client = @db + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author_return_id(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + client = @db + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(ids:) + client = @db + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(ids:, names:) + client = @db + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + client = @db + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil + end + + def truncate_authors + client = @db + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + + def create_book(name:, author_id:) + client = @db + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + + def get_authors_by_book_name(name:) + client = @db + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end + end + + def many_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs index ea35794..a51fa99 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -1,273 +1,193 @@ # auto-generated by sqlc - do not edit module Sqlite3Codegen - GetAuthorSql: String - - class GetAuthorRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow - end - - class GetAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> GetAuthorArgs - end - - ListAuthorsSql: String - - class ListAuthorsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow - end - - CreateAuthorSql: String - - class CreateAuthorArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorArgs - end - - DeleteAuthorSql: String - - class DeleteAuthorArgs < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> DeleteAuthorArgs - end - - ListAuthorsPagedSql: String - - class ListAuthorsPagedRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow - end - - class ListAuthorsPagedArgs < ::Struct - attr_accessor offset: Integer - attr_accessor limit: Integer - def self.new: ( Integer offset, Integer limit) -> ListAuthorsPagedArgs - end - - GetAuthorByNamePatternSql: String - - class GetAuthorByNamePatternRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow - end - - class GetAuthorByNamePatternArgs < ::Struct - attr_accessor name_pattern: String? - def self.new: ( String? name_pattern) -> GetAuthorByNamePatternArgs - end - - CreateAuthorReturnIdSql: String - - class CreateAuthorReturnIdRow < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> CreateAuthorReturnIdRow - end - - class CreateAuthorReturnIdArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CreateAuthorReturnIdArgs - end - - UpdateAuthorBiosSql: String - - class UpdateAuthorBiosArgs < ::Struct - attr_accessor bio: String? - def self.new: ( String? bio) -> UpdateAuthorBiosArgs - end - - GetAuthorsByIdsSql: String - - class GetAuthorsByIdsRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow - end - - class GetAuthorsByIdsArgs < ::Struct - attr_accessor ids: Array[Integer] - def self.new: ( Array[Integer] ids) -> GetAuthorsByIdsArgs - end - - GetAuthorsByIdsAndNamesSql: String - - class GetAuthorsByIdsAndNamesRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow - end - - class GetAuthorsByIdsAndNamesArgs < ::Struct - attr_accessor ids: Array[Integer] - attr_accessor names: Array[String] - def self.new: ( Array[Integer] ids, Array[String] names) -> GetAuthorsByIdsAndNamesArgs - end - - class CopyAuthorsArgs < ::Struct - attr_accessor name: String - attr_accessor bio: String? - def self.new: ( String name, String? bio) -> CopyAuthorsArgs - end - - TruncateAuthorsSql: String - - CreateBookSql: String - - class CreateBookRow < ::Struct - attr_accessor id: Integer - def self.new: ( Integer id) -> CreateBookRow - end - - class CreateBookArgs < ::Struct - attr_accessor name: String - attr_accessor author_id: Integer - def self.new: ( String name, Integer author_id) -> CreateBookArgs - end - - ListAllAuthorsBooksSql: String - - class ListAllAuthorsBooksRow < ::Struct - attr_accessor author: Author - attr_accessor book: Book - def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow - end - - GetDuplicateAuthorsSql: String - - class GetDuplicateAuthorsRow < ::Struct - attr_accessor author: Author - attr_accessor author2: Author - def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow - end - - GetAuthorsByBookNameSql: String - - class GetAuthorsByBookNameRow < ::Struct - attr_accessor id: Integer - attr_accessor name: String - attr_accessor bio: String? - attr_accessor book: Book - def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow - end - - class GetAuthorsByBookNameArgs < ::Struct - attr_accessor name: String - def self.new: ( String name) -> GetAuthorsByBookNameArgs - end - - ManyParamsSql: String - - class ManyParamsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - def self.new: ( Integer c0, String c1) -> ManyParamsRow - end - - class ManyParamsArgs < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> ManyParamsArgs - end - - manyRowsSql: String - - class ManyRowsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - attr_accessor c4: Integer - def self.new: ( - Integer c0, - String c1, - Float c2, - String c3, - Integer c4 - ) -> ManyRowsRow - end - - class ManyRowsArgs < ::Struct - attr_accessor c0: Integer - def self.new: ( Integer c0) -> ManyRowsArgs - end - - FourColsSql: String - - class FourColsRow < ::Struct - attr_accessor c0: Integer - attr_accessor c1: String - attr_accessor c2: Float - attr_accessor c3: String - def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow - end - - class QuerySql - @db: untyped - def initialize: (untyped) -> void - - def transaction: [T] () { (self) -> T } -> T - - def get_author: (GetAuthorArgs) -> GetAuthorRow? - - def list_authors: -> Array[ListAuthorsRow] - - def create_author: (CreateAuthorArgs) -> void - - def delete_author: (DeleteAuthorArgs) -> void - - def list_authors_paged: (ListAuthorsPagedArgs) -> Array[ListAuthorsPagedRow] - - def get_author_by_name_pattern: (GetAuthorByNamePatternArgs) -> Array[GetAuthorByNamePatternRow] - - def create_author_return_id: (CreateAuthorReturnIdArgs) -> Integer - - def update_author_bios: (UpdateAuthorBiosArgs) -> Integer - - def get_authors_by_ids: (GetAuthorsByIdsArgs) -> Array[GetAuthorsByIdsRow] - - def get_authors_by_ids_and_names: (GetAuthorsByIdsAndNamesArgs) -> Array[GetAuthorsByIdsAndNamesRow] - - def copy_authors: (Array[CopyAuthorsArgs]) -> void - - def truncate_authors: -> void - - def create_book: (CreateBookArgs) -> Integer - - def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] - - def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] - - def get_authors_by_book_name: (GetAuthorsByBookNameArgs) -> Array[GetAuthorsByBookNameRow] - - def many_params: (ManyParamsArgs) -> ManyParamsRow? - - def many_rows: (manyRowsArgs) -> Array[manyRowsRow] - - def four_cols: -> Array[FourColsRow] - - private - def slice_placeholders: (Array[untyped]) -> String - end -end \ No newline at end of file + GET_AUTHOR_SQL: String + + class GetAuthorRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorRow + end + + LIST_AUTHORS_SQL: String + + class ListAuthorsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsRow + end + + CREATE_AUTHOR_SQL: String + + DELETE_AUTHOR_SQL: String + + LIST_AUTHORS_PAGED_SQL: String + + class ListAuthorsPagedRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> ListAuthorsPagedRow + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL: String + + class GetAuthorByNamePatternRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorByNamePatternRow + end + + CREATE_AUTHOR_RETURN_ID_SQL: String + + class CreateAuthorReturnIdRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateAuthorReturnIdRow + end + + UPDATE_AUTHOR_BIOS_SQL: String + + GET_AUTHORS_BY_IDS_SQL: String + + class GetAuthorsByIdsRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsRow + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL: String + + class GetAuthorsByIdsAndNamesRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( Integer id, String name, String? bio) -> GetAuthorsByIdsAndNamesRow + end + + class CopyAuthorsArgs < ::Struct + attr_accessor name: String + attr_accessor bio: String? + def self.new: ( String name, String? bio) -> CopyAuthorsArgs + end + + TRUNCATE_AUTHORS_SQL: String + + CREATE_BOOK_SQL: String + + class CreateBookRow < ::Struct + attr_accessor id: Integer + def self.new: ( Integer id) -> CreateBookRow + end + + LIST_ALL_AUTHORS_BOOKS_SQL: String + + class ListAllAuthorsBooksRow < ::Struct + attr_accessor author: Author + attr_accessor book: Book + def self.new: ( Author author, Book book) -> ListAllAuthorsBooksRow + end + + GET_DUPLICATE_AUTHORS_SQL: String + + class GetDuplicateAuthorsRow < ::Struct + attr_accessor author: Author + attr_accessor author2: Author + def self.new: ( Author author, Author author2) -> GetDuplicateAuthorsRow + end + + GET_AUTHORS_BY_BOOK_NAME_SQL: String + + class GetAuthorsByBookNameRow < ::Struct + attr_accessor id: Integer + attr_accessor name: String + attr_accessor bio: String? + attr_accessor book: Book + def self.new: ( Integer id, String name, String? bio, Book book) -> GetAuthorsByBookNameRow + end + + MANY_PARAMS_SQL: String + + class ManyParamsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + def self.new: ( Integer c0, String c1) -> ManyParamsRow + end + + MANY_ROWS_SQL: String + + class ManyRowsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: String + attr_accessor c4: Integer + def self.new: ( + Integer c0, + String c1, + Float c2, + String c3, + Integer c4 + ) -> ManyRowsRow + end + + FOUR_COLS_SQL: String + + class FourColsRow < ::Struct + attr_accessor c0: Integer + attr_accessor c1: String + attr_accessor c2: Float + attr_accessor c3: String + def self.new: ( Integer c0, String c1, Float c2, String c3) -> FourColsRow + end + + class QuerySql + def initialize: (untyped db) -> void + + def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T + + def get_author: (id: Integer) -> GetAuthorRow? + + def list_authors: -> Array[ListAuthorsRow] + + def create_author: (name: String, bio: String?) -> void + + def delete_author: (id: Integer) -> void + + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] + + def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] + + def create_author_return_id: (name: String, bio: String?) -> Integer + + def update_author_bios: (bio: String?) -> Integer + + def get_authors_by_ids: (ids: Array[Integer]) -> Array[GetAuthorsByIdsRow] + + def get_authors_by_ids_and_names: (ids: Array[Integer], names: Array[String]) -> Array[GetAuthorsByIdsAndNamesRow] + + def copy_authors: (Array[CopyAuthorsArgs] rows) -> void + + def truncate_authors: -> void + + def create_book: (name: String, author_id: Integer) -> Integer + + def list_all_authors_books: -> Array[ListAllAuthorsBooksRow] + + def get_duplicate_authors: -> Array[GetDuplicateAuthorsRow] + + def get_authors_by_book_name: (name: String) -> Array[GetAuthorsByBookNameRow] + + def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + + def many_rows: (c0: Integer) -> Array[manyRowsRow] + + def four_cols: -> Array[FourColsRow] + + private + + def slice_placeholders: (Array[untyped] values) -> String + end +end diff --git a/test/golden/sqlite3/ruby32_notypes/models.rb b/test/golden/sqlite3/ruby32_notypes/models.rb index c96b91e..bf6966f 100644 --- a/test/golden/sqlite3/ruby32_notypes/models.rb +++ b/test/golden/sqlite3/ruby32_notypes/models.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit module Sqlite3Codegen - class Author < Data.define(:id, :name, :bio) - end - - class Book < Data.define(:id, :name, :author_id, :description) - end -end \ No newline at end of file + class Author < Data.define(:id, :name, :bio) + end + + class Book < Data.define(:id, :name, :author_id, :description) + end +end diff --git a/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb index b6a7fc5..6ad5713 100644 --- a/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb @@ -1,35 +1,48 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - WithNumbers2GoSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class WithNumbers2GoArgs < Data.define(:c0) - end - - class My_queriesSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def with_numbers2_go(with_numbers2_go_args) - client = @db - query_params = [with_numbers2_go_args.c0] - stmt = client.prepare(WithNumbers2GoSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - end -end \ No newline at end of file + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def with_numbers2_go(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/ruby32_notypes/other_sql.rb b/test/golden/sqlite3/ruby32_notypes/other_sql.rb index bbdfe6f..48bd104 100644 --- a/test/golden/sqlite3/ruby32_notypes/other_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/other_sql.rb @@ -1,85 +1,91 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - NoColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class NoColsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - OtherOneSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class OtherOneRow < Data.define(:c0) - end - - CountRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class CountRowsArgs < Data.define(:c0) - end - - class OtherSql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def no_cols(no_cols_args) - client = @db - query_params = [ - no_cols_args.c0, - no_cols_args.c1, - no_cols_args.c2, - no_cols_args.c3, - no_cols_args.c4 - ] - stmt = client.prepare(NoColsSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def other_one - client = @db - stmt = client.prepare(OtherOneSql) - begin - result = stmt.execute - row = result.first - return nil if row.nil? - entity = OtherOneRow.new(row[0]) - return entity - ensure - stmt.close - end - end - - def count_rows(count_rows_args) - client = @db - query_params = [count_rows_args.c0] - stmt = client.prepare(CountRowsSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - end -end \ No newline at end of file + NO_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + OTHER_ONE_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherOneRow < Data.define(:c0) + end + + COUNT_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class OtherSql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + def no_cols(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def other_one + client = @db + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end + end + + def count_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end +end diff --git a/test/golden/sqlite3/ruby32_notypes/query_sql.rb b/test/golden/sqlite3/ruby32_notypes/query_sql.rb index 3d09e88..8cfab6a 100644 --- a/test/golden/sqlite3/ruby32_notypes/query_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -1,462 +1,450 @@ +# frozen_string_literal: true + # auto-generated by sqlc - do not edit require 'connection_pool' require 'sqlite3' require_relative 'models' module Sqlite3Codegen - GetAuthorSql = %q(SELECT id, name, bio FROM authors - WHERE id = ? LIMIT 1) - - class GetAuthorRow < Data.define(:id, :name, :bio) - end - - class GetAuthorArgs < Data.define(:id) - end - - ListAuthorsSql = %q(SELECT id, name, bio FROM authors - ORDER BY name) - - class ListAuthorsRow < Data.define(:id, :name, :bio) - end - - CreateAuthorSql = %q(INSERT INTO authors ( - name, bio - ) VALUES ( - ?, ? - )) - - class CreateAuthorArgs < Data.define(:name, :bio) - end - - DeleteAuthorSql = %q(DELETE FROM authors - WHERE id = ?) - - class DeleteAuthorArgs < Data.define(:id) - end - - ListAuthorsPagedSql = %q(SELECT id, name, bio FROM authors - ORDER BY name - LIMIT ?2 OFFSET ?1) - - class ListAuthorsPagedRow < Data.define(:id, :name, :bio) - end - - class ListAuthorsPagedArgs < Data.define(:offset, :limit) - end - - GetAuthorByNamePatternSql = %q(SELECT id, name, bio FROM authors - WHERE name LIKE COALESCE(?1, '%')) - - class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) - end - - class GetAuthorByNamePatternArgs < Data.define(:name_pattern) - end - - CreateAuthorReturnIdSql = %q(INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id) - - class CreateAuthorReturnIdRow < Data.define(:id) - end - - class CreateAuthorReturnIdArgs < Data.define(:name, :bio) - end - - UpdateAuthorBiosSql = %q(UPDATE authors - SET bio = ? - WHERE bio IS NOT NULL) - - class UpdateAuthorBiosArgs < Data.define(:bio) - end - - GetAuthorsByIdsSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?)) - - class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsArgs < Data.define(:ids) - end - - GetAuthorsByIdsAndNamesSql = %q(SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?)) - - class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) - end - - class GetAuthorsByIdsAndNamesArgs < Data.define(:ids, :names) - end - - class CopyAuthorsArgs < Data.define(:name, :bio) - end - - TruncateAuthorsSql = %q(DELETE FROM authors) - - CreateBookSql = %q(INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id) - - class CreateBookRow < Data.define(:id) - end - - class CreateBookArgs < Data.define(:name, :author_id) - end - - ListAllAuthorsBooksSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - ORDER BY authors.name) - - class ListAllAuthorsBooksRow < Data.define(:author, :book) - end - - GetDuplicateAuthorsSql = %q(SELECT - authors1.id, authors1.name, authors1.bio, - authors2.id, authors2.name, authors2.bio - FROM authors AS authors1 - INNER JOIN authors AS authors2 ON authors1.name = authors2.name - WHERE authors1.id < authors2.id) - - class GetDuplicateAuthorsRow < Data.define(:author, :author2) - end - - GetAuthorsByBookNameSql = %q(SELECT - authors.id, authors.name, authors.bio, - books.id, books.name, books.author_id, books.description - FROM authors INNER JOIN books ON authors.id = books.author_id - WHERE books.name = ?) - - class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) - end - - class GetAuthorsByBookNameArgs < Data.define(:name) - end - - ManyParamsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyParamsRow < Data.define(:c0, :c1) - end - - class ManyParamsArgs < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - ManyRowsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class ManyRowsRow < Data.define( - :c0, - :c1, - :c2, - :c3, - :c4 - ) - end - - class ManyRowsArgs < Data.define(:c0) - end - - FourColsSql = %q(SELECT 1 -- ? ? - WHERE a = ?) - - class FourColsRow < Data.define(:c0, :c1, :c2, :c3) - end - - class QuerySql - def initialize(db) - @db = db - end - - def transaction - @db.transaction do - yield self - end - end - - def get_author(get_author_args) - client = @db - query_params = [get_author_args.id] - stmt = client.prepare(GetAuthorSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = GetAuthorRow.new(row[0], row[1], row[2]) - return entity - ensure - stmt.close - end - end - - def list_authors - client = @db - stmt = client.prepare(ListAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAuthorsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author(create_author_args) - client = @db - query_params = [create_author_args.name, create_author_args.bio] - stmt = client.prepare(CreateAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def delete_author(delete_author_args) - client = @db - query_params = [delete_author_args.id] - stmt = client.prepare(DeleteAuthorSql) - begin - stmt.execute(*query_params) - ensure - stmt.close - end - end - - def list_authors_paged(list_authors_paged_args) - client = @db - query_params = [list_authors_paged_args.offset, list_authors_paged_args.limit] - stmt = client.prepare(ListAuthorsPagedSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << ListAuthorsPagedRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_author_by_name_pattern(get_author_by_name_pattern_args) - client = @db - query_params = [get_author_by_name_pattern_args.name_pattern] - stmt = client.prepare(GetAuthorByNamePatternSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def create_author_return_id(create_author_return_id_args) - client = @db - query_params = [create_author_return_id_args.name, create_author_return_id_args.bio] - stmt = client.prepare(CreateAuthorReturnIdSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def update_author_bios(update_author_bios_args) - client = @db - query_params = [update_author_bios_args.bio] - stmt = client.prepare(UpdateAuthorBiosSql) - begin - stmt.execute(*query_params) - return client.changes - ensure - stmt.close - end - end - - def get_authors_by_ids(get_authors_by_ids_args) - client = @db - query_params = [*get_authors_by_ids_args.ids] - sql = GetAuthorsByIdsSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_args.ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_ids_and_names(get_authors_by_ids_and_names_args) - client = @db - query_params = [*get_authors_by_ids_and_names_args.ids, *get_authors_by_ids_and_names_args.names] - sql = GetAuthorsByIdsAndNamesSql.sub('/*SLICE:ids*/?', slice_placeholders(get_authors_by_ids_and_names_args.ids)).sub('/*SLICE:names*/?', slice_placeholders(get_authors_by_ids_and_names_args.names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) - end - return entities - ensure - stmt.close - end - end - - def copy_authors(copy_authors_args) - client = @db - copy_authors_args.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |item| [item.name, item.bio] }) - end - end - - def truncate_authors - client = @db - stmt = client.prepare(TruncateAuthorsSql) - begin - stmt.execute - ensure - stmt.close - end - end - - def create_book(create_book_args) - client = @db - query_params = [create_book_args.name, create_book_args.author_id] - stmt = client.prepare(CreateBookSql) - begin - result = stmt.execute(*query_params) - row = result.first - return row[0] - ensure - stmt.close - end - end - - def list_all_authors_books - client = @db - stmt = client.prepare(ListAllAuthorsBooksSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def get_duplicate_authors - client = @db - stmt = client.prepare(GetDuplicateAuthorsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) - end - return entities - ensure - stmt.close - end - end - - def get_authors_by_book_name(get_authors_by_book_name_args) - client = @db - query_params = [get_authors_by_book_name_args.name] - stmt = client.prepare(GetAuthorsByBookNameSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) - end - return entities - ensure - stmt.close - end - end - - def many_params(many_params_args) - client = @db - query_params = [ - many_params_args.c0, - many_params_args.c1, - many_params_args.c2, - many_params_args.c3, - many_params_args.c4 - ] - stmt = client.prepare(ManyParamsSql) - begin - result = stmt.execute(*query_params) - row = result.first - return nil if row.nil? - entity = ManyParamsRow.new(row[0], row[1]) - return entity - ensure - stmt.close - end - end - - def many_rows(many_rows_args) - client = @db - query_params = [many_rows_args.c0] - stmt = client.prepare(manyRowsSql) - begin - result = stmt.execute(*query_params) - entities = [] - result.each do |row| - entities << manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) - end - return entities - ensure - stmt.close - end - end - - def four_cols - client = @db - stmt = client.prepare(FourColsSql) - begin - result = stmt.execute - entities = [] - result.each do |row| - entities << FourColsRow.new(row[0], row[1], row[2], row[3]) - end - return entities - ensure - stmt.close - end - end - - private - - def slice_placeholders(values) - values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') - end - end -end \ No newline at end of file + GET_AUTHOR_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE id = ? LIMIT 1 + SQL + + class GetAuthorRow < Data.define(:id, :name, :bio) + end + + LIST_AUTHORS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + SQL + + class ListAuthorsRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_SQL = <<~'SQL' + INSERT INTO authors ( + name, bio + ) VALUES ( + ?, ? + ) + SQL + + DELETE_AUTHOR_SQL = <<~'SQL' + DELETE FROM authors + WHERE id = ? + SQL + + LIST_AUTHORS_PAGED_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + ORDER BY name + LIMIT ?2 OFFSET ?1 + SQL + + class ListAuthorsPagedRow < Data.define(:id, :name, :bio) + end + + GET_AUTHOR_BY_NAME_PATTERN_SQL = <<~'SQL' + SELECT id, name, bio FROM authors + WHERE name LIKE COALESCE(?1, '%') + SQL + + class GetAuthorByNamePatternRow < Data.define(:id, :name, :bio) + end + + CREATE_AUTHOR_RETURN_ID_SQL = <<~'SQL' + INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id + SQL + + class CreateAuthorReturnIdRow < Data.define(:id) + end + + UPDATE_AUTHOR_BIOS_SQL = <<~'SQL' + UPDATE authors + SET bio = ? + WHERE bio IS NOT NULL + SQL + + GET_AUTHORS_BY_IDS_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) + SQL + + class GetAuthorsByIdsRow < Data.define(:id, :name, :bio) + end + + GET_AUTHORS_BY_IDS_AND_NAMES_SQL = <<~'SQL' + SELECT id, name, bio FROM authors WHERE id IN (/*SLICE:ids*/?) AND name IN (/*SLICE:names*/?) + SQL + + class GetAuthorsByIdsAndNamesRow < Data.define(:id, :name, :bio) + end + + class CopyAuthorsArgs < Data.define(:name, :bio) + end + + TRUNCATE_AUTHORS_SQL = <<~'SQL' + DELETE FROM authors + SQL + + CREATE_BOOK_SQL = <<~'SQL' + INSERT INTO books (name, author_id) VALUES (?, ?) RETURNING id + SQL + + class CreateBookRow < Data.define(:id) + end + + LIST_ALL_AUTHORS_BOOKS_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + ORDER BY authors.name + SQL + + class ListAllAuthorsBooksRow < Data.define(:author, :book) + end + + GET_DUPLICATE_AUTHORS_SQL = <<~'SQL' + SELECT + authors1.id, authors1.name, authors1.bio, + authors2.id, authors2.name, authors2.bio + FROM authors AS authors1 + INNER JOIN authors AS authors2 ON authors1.name = authors2.name + WHERE authors1.id < authors2.id + SQL + + class GetDuplicateAuthorsRow < Data.define(:author, :author2) + end + + GET_AUTHORS_BY_BOOK_NAME_SQL = <<~'SQL' + SELECT + authors.id, authors.name, authors.bio, + books.id, books.name, books.author_id, books.description + FROM authors INNER JOIN books ON authors.id = books.author_id + WHERE books.name = ? + SQL + + class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) + end + + MANY_PARAMS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyParamsRow < Data.define(:c0, :c1) + end + + MANY_ROWS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class ManyRowsRow < Data.define( + :c0, + :c1, + :c2, + :c3, + :c4 + ) + end + + FOUR_COLS_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class FourColsRow < Data.define(:c0, :c1, :c2, :c3) + end + + class QuerySql + # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) + def initialize(db) + @db = db + end + + # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it + # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. + def transaction(mode: nil) + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end + end + + # Returns the author with the given id, or nil when there is none. + def get_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end + end + + def list_authors + client = @db + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def delete_author(id:) + client = @db + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + + def list_authors_paged(offset:, limit:) + client = @db + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_author_by_name_pattern(name_pattern: nil) + client = @db + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def create_author_return_id(name:, bio:) + client = @db + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + # Sets the bio of every author that has one and returns how many were changed. + def update_author_bios(bio:) + client = @db + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + + def get_authors_by_ids(ids:) + client = @db + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + def get_authors_by_ids_and_names(ids:, names:) + client = @db + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + client = @db + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil + end + + def truncate_authors + client = @db + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + end + + def create_book(name:, author_id:) + client = @db + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end + end + + def list_all_authors_books + client = @db + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def get_duplicate_authors + client = @db + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close + end + end + + def get_authors_by_book_name(name:) + client = @db + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + client = @db + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end + end + + def many_rows(c0:) + client = @db + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close + end + end + + def four_cols + client = @db + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close + end + end + + private + + def slice_placeholders(values) + values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') + end + end +end diff --git a/test/golden_test.rb b/test/golden_test.rb index d483417..a95b2ea 100644 --- a/test/golden_test.rb +++ b/test/golden_test.rb @@ -8,10 +8,18 @@ class GoldenTest < Minitest::Test include PluginHelper - { 'pg' => 'pg', 'mysql2' => 'mysql2', 'sqlite3' => 'sqlite3' }.each do |driver, example| + COMMON = %w[models.rb models.rbs query_sql.rb query_sql.rbs].freeze + FILES = { + 'pg' => COMMON + %w[connection.rb connection.rbs], + 'mysql2' => COMMON + %w[connection.rb connection.rbs], + 'sqlite3' => COMMON + }.freeze + + FILES.each do |driver, expected_files| + example = driver define_method("test_#{driver}_matches_examples") do files = generate(load_request(driver)) - assert_equal %w[models.rb models.rbs query_sql.rb query_sql.rbs], files.keys.sort + assert_equal expected_files.sort, files.keys.sort files.each do |name, contents| expected = File.read(File.join(ROOT, 'examples', example, name)) assert_equal normalize(expected), normalize(contents), "examples/#{example}/#{name} is stale" diff --git a/tests/end2end_mysql2.rb b/tests/end2end_mysql2.rb index 1fcc49a..e65efa9 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -1,6 +1,8 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'async' +require 'async/barrier' require_relative 'consts' require_relative 'rbs_types' require_relative '../examples/mysql2/query_sql' @@ -20,36 +22,36 @@ def mysql2_params end def setup - @query_sql = QuerySql.new({}, mysql2_params) + @query_sql = QuerySql.connect(mysql2_params) @query_sql.truncate_authors # cascades to books @query_sql.truncate_extended_bios end def create_author(name, bio = nil) - @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + @query_sql.create_author_return_id(name: name, bio: bio) end # ---- :one / :many / :exec ------------------------------------------------------------------------------------- def test_flow first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) - @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + @query_sql.create_author(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE) assert_equal GetAuthorRow.new(first, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), - @query_sql.get_author(GetAuthorArgs.new(id: first)) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + @query_sql.get_author(id: first) + assert_nil @query_sql.get_author(id: -1) assert_equal [Consts::BOJACK_AUTHOR, Consts::DR_SEUSS_AUTHOR].sort, @query_sql.list_authors.map(&:name).sort - @query_sql.update_author(UpdateAuthorArgs.new(bio: Consts::DR_SEUSS_NEW_QUOTE, id: first)) - assert_equal Consts::DR_SEUSS_NEW_QUOTE, @query_sql.get_author(GetAuthorArgs.new(id: first)).bio + @query_sql.update_author(bio: Consts::DR_SEUSS_NEW_QUOTE, id: first) + assert_equal Consts::DR_SEUSS_NEW_QUOTE, @query_sql.get_author(id: first).bio - @query_sql.delete_author(DeleteAuthorArgs.new(id: first)) + @query_sql.delete_author(id: first) assert_equal [Consts::DR_SEUSS_AUTHOR], @query_sql.list_authors.map(&:name) end def test_nullable_columns_round_trip_nil id = create_author('no bio', nil) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + assert_nil @query_sql.get_author(id: id).bio end def test_one_on_empty_table_returns_nil @@ -58,7 +60,7 @@ def test_one_on_empty_table_returns_nil def test_prepared_statements_are_closed id = create_author('a') - 300.times { @query_sql.get_author(GetAuthorArgs.new(id: id)) } + 300.times { @query_sql.get_author(id: id) } status = Mysql2::Client.new(mysql2_params).query("SHOW GLOBAL STATUS LIKE 'Prepared_stmt_count'").first assert_equal '0', status['Value'].to_s, 'prepared statements leak on the server' end @@ -70,28 +72,28 @@ def test_execlastid_returns_the_new_id second = create_author('b') assert_kind_of Integer, first assert_equal first + 1, second - assert_kind_of Integer, @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: first)) + assert_kind_of Integer, @query_sql.create_book(name: 'book', author_id: first) end def test_execrows_returns_affected_row_count create_author('with bio', 'bio') create_author('with bio 2', 'bio') create_author('without bio', nil) - assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) + assert_equal 2, @query_sql.update_author_bios(bio: 'new bio') end # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- def test_positional_args_limit_and_offset %w[a b c d].each { |name| create_author(name) } - page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + page = @query_sql.list_authors_paged(limit: 2, offset: 1) assert_equal %w[b c], page.map(&:name) end def test_nullable_named_arg %w[abc abd xyz].each { |name| create_author(name) } - assert_equal 3, @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)).size - ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + assert_equal 3, @query_sql.get_author_by_name_pattern(name_pattern: nil).size + ab = @query_sql.get_author_by_name_pattern(name_pattern: 'ab%') assert_equal %w[abc abd], ab.map(&:name).sort end @@ -99,21 +101,21 @@ def test_nullable_named_arg def test_slice ids = %w[a b c].map { |name| create_author(name) } - got = @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: ids.first(2))) + got = @query_sql.get_authors_by_ids(ids: ids.first(2)) assert_equal ids.first(2).sort, got.map(&:id).sort - assert_equal [ids.last], @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [ids.last])).map(&:id) + assert_equal [ids.last], @query_sql.get_authors_by_ids(ids: [ids.last]).map(&:id) end def test_empty_slice_matches_nothing create_author('a') - assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [])) + assert_empty @query_sql.get_authors_by_ids(ids: []) end def test_several_slices_in_one_query ids = %w[a b c].map { |name| create_author(name) } - got = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: %w[b c x])) + got = @query_sql.get_authors_by_ids_and_names(ids: ids, names: %w[b c x]) assert_equal %w[b c], got.map(&:name).sort - assert_empty @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: [])) + assert_empty @query_sql.get_authors_by_ids_and_names(ids: ids, names: []) end # ---- :copyfrom ------------------------------------------------------------------------------------------------- @@ -142,8 +144,8 @@ def test_copyfrom_many_rows_and_none def test_embed first = create_author('a', 'bio a') second = create_author('b') - @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) - @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + @query_sql.create_book(name: 'book 1', author_id: first) + @query_sql.create_book(name: 'book 2', author_id: second) rows = @query_sql.list_all_authors_books assert_equal %w[a b], rows.map { |r| r.author.name } @@ -155,8 +157,8 @@ def test_embed def test_embed_with_plain_columns id = create_author('a', 'bio') - @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) - row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + @query_sql.create_book(name: 'the book', author_id: id) + row = @query_sql.get_authors_by_book_name(name: 'the book').first assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] assert_equal 'the book', row.book.name end @@ -173,10 +175,10 @@ def test_same_table_embedded_twice # ---- enums / sets ---------------------------------------------------------------------------------------------- def test_enum_and_set_columns - @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n1', bio_type: ExtendedBiosBioType::MEMOIR, - author_type: 'Author,Editor')) - @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n2', bio_type: 'Biography', author_type: nil)) - row = @query_sql.get_first_extended_bio_by_type(GetFirstExtendedBioByTypeArgs.new(bio_type: 'Memoir')) + @query_sql.create_extended_bio(author_name: 'a', name: 'n1', bio_type: ExtendedBiosBioType::MEMOIR, + author_type: 'Author,Editor') + @query_sql.create_extended_bio(author_name: 'a', name: 'n2', bio_type: 'Biography', author_type: nil) + row = @query_sql.get_first_extended_bio_by_type(bio_type: 'Memoir') assert_equal GetFirstExtendedBioByTypeRow.new('a', 'n1', 'Memoir', 'Author,Editor'), row assert_equal %w[Autobiography Biography Memoir], ExtendedBiosBioType::ALL assert_equal %w[Author Editor Translator], ExtendedBiosAuthorType::ALL @@ -186,8 +188,8 @@ def test_enum_and_set_columns def test_transaction_commits_and_returns_the_block_value result = @query_sql.transaction do |tx| - id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) - tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + id = tx.create_author_return_id(name: 'in tx', bio: nil) + tx.create_book(name: 'book', author_id: id) :done end assert_equal :done, result @@ -197,7 +199,7 @@ def test_transaction_commits_and_returns_the_block_value def test_transaction_rolls_back_on_error assert_raises(RuntimeError) do @query_sql.transaction do |tx| - tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + tx.create_author_return_id(name: 'rolled back', bio: nil) raise 'boom' end end @@ -205,9 +207,9 @@ def test_transaction_rolls_back_on_error end def test_transaction_is_isolated_until_commit - other = QuerySql.new({}, mysql2_params) + other = QuerySql.connect(mysql2_params) @query_sql.transaction do |tx| - tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'uncommitted', bio: nil)) + tx.create_author_return_id(name: 'uncommitted', bio: nil) assert_empty other.list_authors end assert_equal ['uncommitted'], other.list_authors.map(&:name) @@ -240,6 +242,81 @@ def test_declared_types_match_runtime_values_for_nulls client&.query('DELETE FROM node_mysql_types') end + def test_nested_transaction_rolls_back_to_a_savepoint + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'outer', bio: nil) + assert_raises(RuntimeError) do + tx.transaction do |inner| + inner.create_author_return_id(name: 'inner, rolled back', bio: nil) + raise 'boom' + end + end + tx.create_author_return_id(name: 'outer 2', bio: nil) + end + assert_equal ['outer', 'outer 2'], @query_sql.list_authors.map(&:name).sort + end + + def test_nested_transaction_commits_with_the_outer_one + @query_sql.transaction do |tx| + assert_equal 7, tx.transaction { |inner| inner.create_author_return_id(name: 'inner', bio: nil) && 7 } + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.transaction { |inner| inner.create_author_return_id(name: 'inner 2', bio: nil) } + raise 'boom' + end + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + end + + def test_missing_keyword_arguments_are_errors + assert_raises(ArgumentError) { @query_sql.get_author } + assert_raises(ArgumentError) { @query_sql.get_author(id: 1, unknown: 2) } + end + + def test_named_nullable_arguments_are_optional + create_author('abc') + assert_equal ['abc'], @query_sql.get_author_by_name_pattern.map(&:name) + end + + def test_transaction_isolation_level_is_accepted + assert_equal 1, @query_sql.transaction(isolation: :serializable) { 1 } + assert_raises(KeyError) { @query_sql.transaction(isolation: :bogus) { nil } } + end + + def test_queries_can_share_an_injected_pool + pool = ConnectionPool.new(size: 2) { Mysql2::Client.new(**mysql2_params.transform_keys(&:to_sym)) } + first = QuerySql.new(pool) + second = QuerySql.new(pool) + id = first.create_author_return_id(name: 'shared', bio: nil) + assert_equal 'shared', second.get_author(id: id).name + end + + # The generated classes are plain synchronous Ruby; under the Fiber scheduler (the `async` gem) the pooled + # queries overlap because mysql2 and connection_pool are fiber aware. + def test_queries_run_concurrently_under_the_fiber_scheduler + queries = QuerySql.connect(mysql2_params, size: 5) + pool = queries.instance_variable_get(:@db) + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Sync do + barrier = Async::Barrier.new + 5.times { barrier.async { pool.with { |client| client.query('select sleep(0.4)') } } } + barrier.wait + end + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started + assert_operator elapsed, :<, 1.5, "5 x 0.4s queries took #{elapsed}s: they ran one after the other" + + names = Sync do + barrier = Async::Barrier.new + tasks = Array.new(10) { |i| barrier.async { queries.create_author_return_id(name: "fiber #{i}", bio: nil) } } + barrier.wait + tasks.map(&:wait) + end + assert_equal 10, names.uniq.size + assert_equal 10, @query_sql.list_authors.size + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index 1aa66dc..8f33023 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -1,6 +1,8 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'async' +require 'async/barrier' require 'bigdecimal' require 'ipaddr' require 'securerandom' @@ -23,36 +25,36 @@ def pg_params end def setup - @query_sql = QuerySql.new({}, pg_params) + @query_sql = QuerySql.connect(pg_params) @query_sql.truncate_authors # cascades to books @query_sql.truncate_extended_bios @query_sql.truncate_extra_types end def create_author(name, bio = nil) - @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + @query_sql.create_author_return_id(name: name, bio: bio) end # ---- :one / :many / :exec ------------------------------------------------------------------------------------- def test_flow - first = @query_sql.create_author(CreateAuthorArgs.new(name: Consts::BOJACK_AUTHOR, bio: Consts::BOJACK_THEME)) + first = @query_sql.create_author(name: Consts::BOJACK_AUTHOR, bio: Consts::BOJACK_THEME) assert_equal CreateAuthorRow.new(first.id, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), first - second = @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + second = @query_sql.create_author(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE) assert_equal GetAuthorRow.new(first.id, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), - @query_sql.get_author(GetAuthorArgs.new(id: first.id)) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + @query_sql.get_author(id: first.id) + assert_nil @query_sql.get_author(id: -1) assert_equal [second.name, first.name].sort, @query_sql.list_authors.map(&:name).sort - @query_sql.delete_author(DeleteAuthorArgs.new(id: first.id)) + @query_sql.delete_author(id: first.id) assert_equal [ListAuthorsRow.new(second.id, Consts::DR_SEUSS_AUTHOR, Consts::DR_SEUSS_QUOTE)], @query_sql.list_authors end def test_nullable_columns_round_trip_nil id = create_author('no bio', nil) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + assert_nil @query_sql.get_author(id: id).bio end def test_one_on_empty_table_returns_nil @@ -70,7 +72,7 @@ def test_execlastid_returns_the_returning_column def test_execlastid_with_uuid_key author_id = create_author('a') - book_id = @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: author_id)) + book_id = @query_sql.create_book(name: 'book', author_id: author_id) assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, book_id) end @@ -78,23 +80,23 @@ def test_execrows_returns_affected_row_count create_author('with bio', 'bio') create_author('with bio 2', 'bio') create_author('without bio', nil) - assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) - assert_equal 0, (@query_sql.truncate_authors; @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'x'))) + assert_equal 2, @query_sql.update_author_bios(bio: 'new bio') + assert_equal 0, (@query_sql.truncate_authors; @query_sql.update_author_bios(bio: 'x')) end # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- def test_named_args_limit_and_offset %w[a b c d].each { |name| create_author(name) } - page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + page = @query_sql.list_authors_paged(limit: 2, offset: 1) assert_equal %w[b c], page.map(&:name) end def test_nullable_named_arg %w[abc abd xyz].each { |name| create_author(name) } - all = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)) + all = @query_sql.get_author_by_name_pattern(name_pattern: nil) assert_equal 3, all.size - ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + ab = @query_sql.get_author_by_name_pattern(name_pattern: 'ab%') assert_equal %w[abc abd], ab.map(&:name).sort end @@ -103,9 +105,9 @@ def test_nullable_named_arg def test_array_parameters ids = %w[a b c].map { |name| create_author(name) } assert_equal ids.first(2).sort, - @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(param1: ids.first(2))).map(&:id).sort - assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(param1: [])) - both = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(param1: ids, param2: %w[b c x])) + @query_sql.get_authors_by_ids(param1: ids.first(2)).map(&:id).sort + assert_empty @query_sql.get_authors_by_ids(param1: []) + both = @query_sql.get_authors_by_ids_and_names(param1: ids, param2: %w[b c x]) assert_equal %w[b c], both.map(&:name).sort end @@ -134,8 +136,8 @@ def test_copyfrom_many_rows_and_none def test_embed first = create_author('a', 'bio a') second = create_author('b') - @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) - @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + @query_sql.create_book(name: 'book 1', author_id: first) + @query_sql.create_book(name: 'book 2', author_id: second) rows = @query_sql.list_all_authors_books assert_equal %w[a b], rows.map { |r| r.author.name } @@ -148,8 +150,8 @@ def test_embed def test_embed_with_plain_columns id = create_author('a', 'bio') - @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) - row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + @query_sql.create_book(name: 'the book', author_id: id) + row = @query_sql.get_authors_by_book_name(name: 'the book').first assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] assert_equal 'the book', row.book.name end @@ -166,16 +168,16 @@ def test_same_table_embedded_twice # ---- enums ----------------------------------------------------------------------------------------------------- def test_enum_column - @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n1', bio_type: ExtendedBioType::MEMOIR)) - @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n2', bio_type: ExtendedBioType::BIOGRAPHY)) - row = @query_sql.get_first_extended_bio_by_type(GetFirstExtendedBioByTypeArgs.new(bio_type: 'Biography')) + @query_sql.create_extended_bio(author_name: 'a', name: 'n1', bio_type: ExtendedBioType::MEMOIR) + @query_sql.create_extended_bio(author_name: 'a', name: 'n2', bio_type: ExtendedBioType::BIOGRAPHY) + row = @query_sql.get_first_extended_bio_by_type(bio_type: 'Biography') assert_equal GetFirstExtendedBioByTypeRow.new('a', 'n2', 'Biography'), row assert_equal %w[Autobiography Biography Memoir], ExtendedBioType::ALL end def test_enum_rejects_unknown_values assert_raises(PG::InvalidTextRepresentation) do - @query_sql.create_extended_bio(CreateExtendedBioArgs.new(author_name: 'a', name: 'n', bio_type: 'Fiction')) + @query_sql.create_extended_bio(author_name: 'a', name: 'n', bio_type: 'Fiction') end end @@ -183,8 +185,8 @@ def test_enum_rejects_unknown_values def test_transaction_commits_and_returns_the_block_value result = @query_sql.transaction do |tx| - id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) - tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + id = tx.create_author_return_id(name: 'in tx', bio: nil) + tx.create_book(name: 'book', author_id: id) :done end assert_equal :done, result @@ -194,7 +196,7 @@ def test_transaction_commits_and_returns_the_block_value def test_transaction_rolls_back_on_error assert_raises(RuntimeError) do @query_sql.transaction do |tx| - tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + tx.create_author_return_id(name: 'rolled back', bio: nil) raise 'boom' end end @@ -202,9 +204,9 @@ def test_transaction_rolls_back_on_error end def test_transaction_is_isolated_until_commit - other = QuerySql.new({}, pg_params) + other = QuerySql.connect(pg_params) @query_sql.transaction do |tx| - tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'uncommitted', bio: nil)) + tx.create_author_return_id(name: 'uncommitted', bio: nil) assert_empty other.list_authors end assert_equal ['uncommitted'], other.list_authors.map(&:name) @@ -213,7 +215,7 @@ def test_transaction_is_isolated_until_commit # ---- connection pool ------------------------------------------------------------------------------------------- def test_every_pooled_connection_prepares_its_own_statements - pooled = QuerySql.new({ size: 3 }, pg_params) + pooled = QuerySql.connect(pg_params, size: 3) pool = pooled.instance_variable_get(:@db) held = Queue.new gates = Array.new(3) { Queue.new } @@ -240,10 +242,10 @@ def test_every_pooled_connection_prepares_its_own_statements def test_extra_types_round_trip uuid = '0e52082f-bf69-480e-aff0-1a85139a1074' time = Time.utc(2024, 5, 6, 7, 8, 9) - @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + @query_sql.insert_extra_types( c_uuid: uuid, c_jsonb: { 'a' => [1, 2, { 'b' => nil }] }.to_json, c_text_array: ['x', nil, 'y z'], c_integer_array: [1, 2, 3], c_timestamptz: time, c_inet: '10.1.2.3', c_money: '12.50', c_not_null: 'nn' - )) + ) row = @query_sql.get_extra_types assert_equal uuid, row.c_uuid assert_equal({ 'a' => [1, 2, { 'b' => nil }] }, row.c_jsonb) @@ -256,10 +258,10 @@ def test_extra_types_round_trip end def test_extra_types_nulls - @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + @query_sql.insert_extra_types( c_uuid: nil, c_jsonb: nil, c_text_array: nil, c_integer_array: nil, c_timestamptz: nil, c_inet: nil, c_money: nil, c_not_null: 'x' - )) + ) row = @query_sql.get_extra_types assert_equal [nil] * 7, row.to_h.values_at(:c_uuid, :c_jsonb, :c_text_array, :c_integer_array, :c_timestamptz, :c_inet, :c_money) @@ -298,14 +300,94 @@ def test_declared_types_match_runtime_values_for_nulls end def test_extra_types_match_declared_types - @query_sql.insert_extra_types(InsertExtraTypesArgs.new( + @query_sql.insert_extra_types( c_uuid: SecureRandom.uuid, c_jsonb: '[1]', c_text_array: %w[a], c_integer_array: [1], c_timestamptz: Time.now, c_inet: '::1', c_money: '1', c_not_null: 'x' - )) + ) assert_empty RbsTypes.mismatches(@query_sql.get_extra_types, File.expand_path('../examples/pg/query_sql.rbs', __dir__), 'GetExtraTypesRow') end + def test_nested_transaction_rolls_back_to_a_savepoint + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'outer', bio: nil) + assert_raises(RuntimeError) do + tx.transaction do |inner| + inner.create_author_return_id(name: 'inner, rolled back', bio: nil) + raise 'boom' + end + end + tx.create_author_return_id(name: 'outer 2', bio: nil) + end + assert_equal ['outer', 'outer 2'], @query_sql.list_authors.map(&:name).sort + end + + def test_nested_transaction_commits_with_the_outer_one + @query_sql.transaction do |tx| + assert_equal 7, tx.transaction { |inner| inner.create_author_return_id(name: 'inner', bio: nil) && 7 } + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.transaction { |inner| inner.create_author_return_id(name: 'inner 2', bio: nil) } + raise 'boom' + end + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + end + + def test_missing_keyword_arguments_are_errors + assert_raises(ArgumentError) { @query_sql.get_author } + assert_raises(ArgumentError) { @query_sql.get_author(id: 1, unknown: 2) } + end + + def test_named_nullable_arguments_are_optional + create_author('abc') + assert_equal ['abc'], @query_sql.get_author_by_name_pattern.map(&:name) + end + + def test_transaction_isolation_level + @query_sql.transaction(isolation: :serializable) do + level = @query_sql.instance_variable_get(:@db).with { |c| c.exec('SHOW transaction_isolation').getvalue(0, 0) } + assert_equal 'serializable', level + end + assert_raises(KeyError) { @query_sql.transaction(isolation: :bogus) { nil } } + end + + def test_queries_can_share_an_injected_pool + pool = ConnectionPool.new(size: 2) { PG.connect(**pg_params.transform_keys(&:to_sym)) } + first = QuerySql.new(pool) + second = QuerySql.new(pool) # same connections and statement names: must not clash + id = first.create_author_return_id(name: 'shared', bio: nil) + assert_equal 'shared', second.get_author(id: id).name + assert_equal 'shared', first.get_author(id: id).name + assert_kind_of Integer, id # connection was configured on first use (typed results) + end + + # The generated classes are plain synchronous Ruby; under the Fiber scheduler (the `async` gem) the pooled + # queries overlap because pg and connection_pool are fiber aware. + def test_queries_run_concurrently_under_the_fiber_scheduler + queries = QuerySql.connect(pg_params, size: 5) + pool = queries.instance_variable_get(:@db) + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Sync do + barrier = Async::Barrier.new + 5.times { barrier.async { pool.with { |client| client.exec('select pg_sleep(0.4)') } } } + barrier.wait + end + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started + assert_operator elapsed, :<, 1.5, "5 x 0.4s queries took #{elapsed}s: they ran one after the other" + + names = Sync do + barrier = Async::Barrier.new + tasks = Array.new(10) { |i| barrier.async { queries.create_author_return_id(name: "fiber #{i}", bio: nil) } } + barrier.wait + tasks.map(&:wait) + end + assert_equal 10, names.uniq.size + assert_equal 10, @query_sql.list_authors.size + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_sqlite3.rb b/tests/end2end_sqlite3.rb index 8536c88..6bee695 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -19,27 +19,27 @@ def setup end def create_author(name, bio = nil) - @query_sql.create_author_return_id(CreateAuthorReturnIdArgs.new(name: name, bio: bio)) + @query_sql.create_author_return_id(name: name, bio: bio) end # ---- :one / :many / :exec ------------------------------------------------------------------------------------- def test_flow first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) - @query_sql.create_author(CreateAuthorArgs.new(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE)) + @query_sql.create_author(name: Consts::DR_SEUSS_AUTHOR, bio: Consts::DR_SEUSS_QUOTE) assert_equal GetAuthorRow.new(first, Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME), - @query_sql.get_author(GetAuthorArgs.new(id: first)) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: -1)) + @query_sql.get_author(id: first) + assert_nil @query_sql.get_author(id: -1) assert_equal [Consts::BOJACK_AUTHOR, Consts::DR_SEUSS_AUTHOR].sort, @query_sql.list_authors.map(&:name).sort - @query_sql.delete_author(DeleteAuthorArgs.new(id: first)) + @query_sql.delete_author(id: first) assert_equal [Consts::DR_SEUSS_AUTHOR], @query_sql.list_authors.map(&:name) end def test_nullable_columns_round_trip_nil id = create_author('no bio', nil) - assert_nil @query_sql.get_author(GetAuthorArgs.new(id: id)).bio + assert_nil @query_sql.get_author(id: id).bio end # ---- :execlastid / :execrows ----------------------------------------------------------------------------------- @@ -49,28 +49,28 @@ def test_execlastid_returns_the_returning_column second = create_author('b') assert_kind_of Integer, first assert_equal first + 1, second - assert_kind_of Integer, @query_sql.create_book(CreateBookArgs.new(name: 'book', author_id: first)) + assert_kind_of Integer, @query_sql.create_book(name: 'book', author_id: first) end def test_execrows_returns_affected_row_count create_author('with bio', 'bio') create_author('with bio 2', 'bio') create_author('without bio', nil) - assert_equal 2, @query_sql.update_author_bios(UpdateAuthorBiosArgs.new(bio: 'new bio')) + assert_equal 2, @query_sql.update_author_bios(bio: 'new bio') end # ---- sqlc.arg / sqlc.narg -------------------------------------------------------------------------------------- def test_numbered_parameters_limit_and_offset %w[a b c d].each { |name| create_author(name) } - page = @query_sql.list_authors_paged(ListAuthorsPagedArgs.new(limit: 2, offset: 1)) + page = @query_sql.list_authors_paged(limit: 2, offset: 1) assert_equal %w[b c], page.map(&:name) end def test_nullable_named_arg %w[abc abd xyz].each { |name| create_author(name) } - assert_equal 3, @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: nil)).size - ab = @query_sql.get_author_by_name_pattern(GetAuthorByNamePatternArgs.new(name_pattern: 'ab%')) + assert_equal 3, @query_sql.get_author_by_name_pattern(name_pattern: nil).size + ab = @query_sql.get_author_by_name_pattern(name_pattern: 'ab%') assert_equal %w[abc abd], ab.map(&:name).sort end @@ -78,18 +78,18 @@ def test_nullable_named_arg def test_slice ids = %w[a b c].map { |name| create_author(name) } - got = @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: ids.first(2))) + got = @query_sql.get_authors_by_ids(ids: ids.first(2)) assert_equal ids.first(2).sort, got.map(&:id).sort end def test_empty_slice_matches_nothing create_author('a') - assert_empty @query_sql.get_authors_by_ids(GetAuthorsByIdsArgs.new(ids: [])) + assert_empty @query_sql.get_authors_by_ids(ids: []) end def test_several_slices_in_one_query ids = %w[a b c].map { |name| create_author(name) } - got = @query_sql.get_authors_by_ids_and_names(GetAuthorsByIdsAndNamesArgs.new(ids: ids, names: %w[b c x])) + got = @query_sql.get_authors_by_ids_and_names(ids: ids, names: %w[b c x]) assert_equal %w[b c], got.map(&:name).sort end @@ -117,8 +117,8 @@ def test_copyfrom_is_batched_below_the_bound_parameter_limit def test_embed first = create_author('a', 'bio a') second = create_author('b') - @query_sql.create_book(CreateBookArgs.new(name: 'book 1', author_id: first)) - @query_sql.create_book(CreateBookArgs.new(name: 'book 2', author_id: second)) + @query_sql.create_book(name: 'book 1', author_id: first) + @query_sql.create_book(name: 'book 2', author_id: second) rows = @query_sql.list_all_authors_books assert_equal %w[a b], rows.map { |r| r.author.name } @@ -129,8 +129,8 @@ def test_embed def test_embed_with_plain_columns id = create_author('a', 'bio') - @query_sql.create_book(CreateBookArgs.new(name: 'the book', author_id: id)) - row = @query_sql.get_authors_by_book_name(GetAuthorsByBookNameArgs.new(name: 'the book')).first + @query_sql.create_book(name: 'the book', author_id: id) + row = @query_sql.get_authors_by_book_name(name: 'the book').first assert_equal [id, 'a', 'bio'], [row.id, row.name, row.bio] assert_equal 'the book', row.book.name end @@ -148,8 +148,8 @@ def test_same_table_embedded_twice def test_transaction_commits_and_returns_the_block_value result = @query_sql.transaction do |tx| - id = tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'in tx', bio: nil)) - tx.create_book(CreateBookArgs.new(name: 'book', author_id: id)) + id = tx.create_author_return_id(name: 'in tx', bio: nil) + tx.create_book(name: 'book', author_id: id) :done end assert_equal :done, result @@ -159,7 +159,7 @@ def test_transaction_commits_and_returns_the_block_value def test_transaction_rolls_back_on_error assert_raises(RuntimeError) do @query_sql.transaction do |tx| - tx.create_author_return_id(CreateAuthorReturnIdArgs.new(name: 'rolled back', bio: nil)) + tx.create_author_return_id(name: 'rolled back', bio: nil) raise 'boom' end end @@ -173,6 +173,48 @@ def test_declared_types_match_runtime_values @query_sql.list_authors.each { |row| assert_empty RbsTypes.mismatches(row, rbs, 'ListAuthorsRow') } end + def test_nested_transaction_rolls_back_to_a_savepoint + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'outer', bio: nil) + assert_raises(RuntimeError) do + tx.transaction do |inner| + inner.create_author_return_id(name: 'inner, rolled back', bio: nil) + raise 'boom' + end + end + tx.create_author_return_id(name: 'outer 2', bio: nil) + end + assert_equal ['outer', 'outer 2'], @query_sql.list_authors.map(&:name).sort + end + + def test_nested_transaction_commits_with_the_outer_one + @query_sql.transaction do |tx| + assert_equal 7, tx.transaction { |inner| inner.create_author_return_id(name: 'inner', bio: nil) && 7 } + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.transaction { |inner| inner.create_author_return_id(name: 'inner 2', bio: nil) } + raise 'boom' + end + end + assert_equal ['inner'], @query_sql.list_authors.map(&:name) + end + + def test_missing_keyword_arguments_are_errors + assert_raises(ArgumentError) { @query_sql.get_author } + assert_raises(ArgumentError) { @query_sql.get_author(id: 1, unknown: 2) } + end + + def test_named_nullable_arguments_are_optional + create_author('abc') + assert_equal ['abc'], @query_sql.get_author_by_name_pattern.map(&:name) + end + + def test_transaction_mode + assert_equal 1, @query_sql.transaction(mode: :immediate) { 1 } + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/rbs_types.rb b/tests/rbs_types.rb index 23d2b10..8fb14e3 100644 --- a/tests/rbs_types.rb +++ b/tests/rbs_types.rb @@ -10,7 +10,7 @@ module RbsTypes # { 'c_date' => 'Date?', ... } for `class ` in an .rbs file def attributes(rbs_path, class_name) - body = File.read(rbs_path)[/^\t+class #{class_name}\n(.*?)^\t+end$/m, 1] or raise "#{class_name} not in #{rbs_path}" + body = File.read(rbs_path)[/^( *)class #{class_name}\n(.*?)^\1end$/m, 2] or raise "#{class_name} not in #{rbs_path}" body.scan(/attr_(?:reader|accessor) (\w+): (.+)$/).to_h end From bbf7371e6e1d54337ce2d2d9bb295865197ba618 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:43:20 +0000 Subject: [PATCH 11/15] Rework CI after the C# project's: Build + Tests workflows, service containers, drift checks, options matrix The previous CI run failed in run_codegen.sh: `find examples -name "*.rb"` returns a directory once per file (models.rb, connection.rb, query_sql.rb now), so the yq loop appended bogus `sql:` entries ("unknown engine"). - scripts/tests/run_codegen.rb (replaces the yq script): generates into a scratch dir with the given options and checks the output (valid Ruby, models/connection load, .rbs / Gemfile iff requested) - workflows split like sqlc-gen-csharp: build.yml (WASM build, lint, codegen tests: `sqlc diff` for the WASM and the process plugin, Steep, options matrix on Ruby 3.1/3.2/3.3) and tests.yml (unit tests, plugin on Ruby 3.1/3.2/3.4, end-to-end against PostgreSQL / MySQL service containers and SQLite) - least-privilege permissions, concurrency groups (cancel superseded PR runs), tool versions in scripts/tool-versions.env (also used by build.sh), current action versions - dependabot (actions, bundler), PR and issue templates, docs generation in Ruby (no yq), update_sha.sh without yq - rake end2end_{pg,mysql2,sqlite3}; csv in the Gemfile (a bundled gem since Ruby 3.4) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .github/ISSUE_TEMPLATE/bug_report.yml | 31 +++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 16 +++ .github/dependabot.yml | 16 +++ .github/pull_request_template.md | 8 ++ .github/workflows/build.yml | 146 ++++++++++++++++++++ .github/workflows/ci.yml | 146 -------------------- .github/workflows/docs.yml | 24 ++-- .github/workflows/release.yml | 8 +- .github/workflows/tests.yml | 152 +++++++++++++++++++++ .gitignore | 4 +- Gemfile | 1 + README.md | 48 +++---- Rakefile | 13 ++ docs/Examples.md | 1 - scripts/docs/generate_examples.rb | 30 ++++ scripts/docs/generate_examples.sh | 29 ---- scripts/tests/run_codegen.rb | 73 ++++++++++ scripts/tests/run_codegen.sh | 70 ---------- scripts/tests/run_end2end.sh | 29 ++-- scripts/tool-versions.env | 5 + scripts/wasm/build.sh | 4 +- scripts/wasm/update_sha.sh | 7 +- 23 files changed, 558 insertions(+), 304 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/tests.yml create mode 100644 scripts/docs/generate_examples.rb delete mode 100755 scripts/docs/generate_examples.sh create mode 100755 scripts/tests/run_codegen.rb delete mode 100755 scripts/tests/run_codegen.sh create mode 100644 scripts/tool-versions.env diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..03d2920 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,31 @@ +name: Bug report +description: The plugin fails, or the generated Ruby code is wrong +labels: [bug] +body: + - type: input + id: versions + attributes: + label: Versions + description: Plugin release, sqlc, Ruby, database driver gem + validations: + required: true + - type: textarea + id: config + attributes: + label: sqlc config (the plugin `options:`) + render: yaml + validations: + required: true + - type: textarea + id: sql + attributes: + label: Schema and query that reproduce it + render: sql + validations: + required: true + - type: textarea + id: actual + attributes: + label: What was generated / what happened, and what you expected + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..e266ba5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,16 @@ +name: Feature request +description: Support another sqlc feature, driver or Ruby idiom in the generated code +labels: [enhancement] +body: + - type: textarea + id: what + attributes: + label: What would you like the generated code to support? + description: Show how you would like to call it from Ruby. + validations: + required: true + - type: textarea + id: context + attributes: + label: Context + description: Database, driver gem, framework (Rails, Sinatra, Roda, ...), links. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7c299b8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] + - package-ecosystem: bundler + directory: / + schedule: + interval: weekly + groups: + gems: + patterns: ["*"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b60a608 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +## Summary + + +## Checklist +- [ ] `examples/` is regenerated (`rake sqlc_generate_process`) and committed +- [ ] Tests added or updated (`rake test`, and `rake end2end_*` when the generated code changes) +- [ ] The generated code stays RuboCop-clean (`rake rubocop`) and Steep-clean (`rake steep`) +- [ ] `[release]` / `[minor]` / `[major]` in the merge commit message only if this should create a release diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6f4d526 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,146 @@ +name: Build +defaults: + run: + shell: bash + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "*" ] + paths: + - "**" + - "!**.md" + +jobs: + build-wasm: + name: Build (WASM) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + # the plugin (pure Ruby) + the prebuilt ruby.wasm interpreter, packed into one self-contained module + - name: Build WASM plugin + run: ./scripts/wasm/build.sh + + - name: upload wasm plugin as artifact + uses: actions/upload-artifact@v7 + with: + name: wasm-file + path: dist/plugin.wasm + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Ruby syntax and warnings (plugin and scripts) + run: | + set -e + for file in $(git ls-files '*.rb' ':!examples' ':!test/golden'); do + ruby -wc "${file}" > /dev/null + done + + # the code generated into examples/ has to stay RuboCop-clean + - name: RuboCop (generated code) + run: bundle exec rake rubocop + + codegen-tests: + name: Codegen Tests + needs: [build-wasm] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Load tool versions + run: cat scripts/tool-versions.env >> "$GITHUB_ENV" + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - uses: sqlc-dev/setup-sqlc@v5 + with: + sqlc-version: '${{ env.SQLC_VERSION }}' + + - uses: actions/download-artifact@v8 + with: + name: wasm-file + path: dist + + - name: Updating plugin sha + run: ./scripts/wasm/update_sha.sh sqlc.ci.yaml + + # examples/ is the plugin's output for examples/authors: it must be committed in sync with the plugin + - name: Verify pushed generated code is synced (WASM plugin) + run: sqlc -f sqlc.ci.yaml diff + + - name: Verify pushed generated code is synced (process plugin) + run: sqlc -f sqlc.local.yaml diff + + - name: Steep check + run: bundle exec rake steep + + codegen-options: + name: Codegen Options (ruby ${{ matrix.ruby-version }}, types=${{ matrix.generate-types }}, gemfile=${{ matrix.generate-gemfile }}) + needs: [build-wasm] + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - { ruby-version: '3.1', generate-types: 'true', generate-gemfile: 'true' } + - { ruby-version: '3.2', generate-types: 'false', generate-gemfile: 'false' } + - { ruby-version: '3.3', generate-types: 'true', generate-gemfile: 'false' } + - { ruby-version: '3.3', generate-types: 'false', generate-gemfile: 'true' } + + steps: + - uses: actions/checkout@v7 + + - name: Load tool versions + run: cat scripts/tool-versions.env >> "$GITHUB_ENV" + + # the generated code for rubyVersion X is checked with a Ruby X interpreter + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + + - uses: sqlc-dev/setup-sqlc@v5 + with: + sqlc-version: '${{ env.SQLC_VERSION }}' + + - uses: actions/download-artifact@v8 + with: + name: wasm-file + path: dist + + - name: Updating plugin sha + run: ./scripts/wasm/update_sha.sh sqlc.ci.yaml + + - name: Generate with these options and check the output + run: >- + ./scripts/tests/run_codegen.rb sqlc.ci.yaml + "${{ matrix.ruby-version }}" ${{ matrix.generate-types }} ${{ matrix.generate-gemfile }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 9bd2332..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: CI -defaults: - run: - shell: bash - -permissions: - contents: write - pull-requests: write - -on: - push: - branches: [ "main" ] - paths: - - "**" - - "!**.md" - pull_request: - branches: [ "*" ] - paths: - - "**" - - "!**.md" - -jobs: - test: - name: Plugin Tests - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - # unit tests + golden tests - - name: Unit & golden tests - run: bundle exec rake test - - build: - name: Build - runs-on: ubuntu-latest - needs: [test] - - steps: - - uses: actions/checkout@v4 - - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3' - bundler-cache: true - - # pure Ruby plugin + prebuilt ruby.wasm interpreter, packed into one self-contained module - - name: Build WASM plugin - run: ./scripts/wasm/build.sh - - - name: upload wasm plugin as artifact - uses: actions/upload-artifact@v4 - with: - name: wasm-file - path: dist/plugin.wasm - - codegen_test: - name: Codegen Test - needs: [build] - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - ruby-version: [ '3.3' ] - generate-types: [ 'true' ] - generate-gemfile: [ 'false' ] - - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - - name: Load .env file - uses: xom9ikk/dotenv@v2.3.0 - with: - load-mode: strict - - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true - - - uses: actions/download-artifact@v4 - with: - name: wasm-file - path: dist - - - uses: sqlc-dev/setup-sqlc@v4 - with: - sqlc-version: '1.27.0' - - - name: Updating plugin sha - run: ./scripts/wasm/update_sha.sh ${SQLC_CI_FILE} - - - name: Codegen Test - run: ./scripts/tests/run_codegen.sh ${SQLC_CI_FILE} \ - ${{ matrix.ruby-version }} ${{ matrix.generate-types }} ${{ matrix.generate-gemfile }} - - - name: RuboCop (generated code) - run: rake rubocop - - - name: Steep check - if: ${{ matrix.generate-types == 'true' }} - run: rake steep - - end2end_tests: - name: End-to-End Tests - runs-on: ubuntu-latest - needs: [build, codegen_test] - - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: wasm-file - path: dist - - - uses: sqlc-dev/setup-sqlc@v4 - with: - sqlc-version: '1.27.0' - - - name: Load .env file - uses: xom9ikk/dotenv@v2.3.0 - with: - load-mode: strict - - - name: Verify pushed generated code is synced - run: | - ./scripts/wasm/update_sha.sh ${SQLC_CI_FILE} - sqlc -f ${SQLC_CI_FILE} diff - - - name: Docker compose - uses: hoverkraft-tech/compose-action@v1.5.1 - with: - services: | - mysqldb - postgresdb - plugin-tests - - - name: End-to-End Tests - run: ./scripts/tests/run_end2end.sh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0c525cd..fd8151e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,7 @@ name: Docs + +concurrency: docs-regeneration + defaults: run: shell: bash @@ -8,32 +11,37 @@ permissions: pull-requests: write on: + workflow_dispatch: push: branches: [ "main" ] paths: - "sqlc.ci.yaml" - - "examples/**" - - "tests/**" - - "lib/**" + - "examples/authors/**" + - "scripts/docs/**" jobs: - generate-and-push-docs: + generate-docs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' - name: Updating Examples doc - run: ./scripts/docs/generate_examples.sh docs/Examples.md + run: ruby scripts/docs/generate_examples.rb docs/Examples.md - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + base: main title: update auto-generated docs commit-message: update docs branch: update-docs branch-suffix: timestamp - delete-branch: true \ No newline at end of file + delete-branch: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 952bb0a..eb20011 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: on: workflow_run: - workflows: [CI] + workflows: [Build] types: [completed] branches: [main] @@ -21,7 +21,7 @@ jobs: IS_RELEASE: ${{ steps.check_msg.outputs.IS_RELEASE }} steps: - name: Check out Git repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Check latest commit message id: check_msg @@ -45,7 +45,7 @@ jobs: IS_RELEASE: ${{ needs.check_commit.outputs.IS_RELEASE }} steps: - name: Check out Git repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -54,7 +54,7 @@ jobs: uses: dawidd6/action-download-artifact@v6 with: github_token: ${{secrets.GITHUB_TOKEN}} - workflow: ci.yml + workflow: build.yml workflow_conclusion: success name: wasm-file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c9889c9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,152 @@ +name: Tests +defaults: + run: + shell: bash + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "*" ] + paths: + - "**" + - "!**.md" + +env: + DB_NAME: tests + DB_USER: root + DB_PASS: pass + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + # protocol codec, generator, golden snapshots, formatting rules (incl. the protobuf codec cross-check) + - name: Unit & golden tests + run: bundle exec rake test + + compatibility: + name: Plugin on Ruby ${{ matrix.ruby-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby-version: ['3.1', '3.2', '3.4'] + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + + # the process plugin runs on whatever Ruby the user has: no bundler, only the standard library + minitest + - name: Unit tests + run: ruby -Ilib -Itest -e 'Dir["test/*_test.rb"].sort.each { |file| require File.expand_path(file) }' + + end2end-pg: + name: End-to-End (PostgreSQL) + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16.2 + env: + POSTGRES_DB: tests + POSTGRES_USER: root + POSTGRES_PASSWORD: pass + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U root -d tests" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + POSTGRES_HOST: 127.0.0.1 + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Create the schema + run: psql -v ON_ERROR_STOP=1 -h 127.0.0.1 -U "${DB_USER}" -d "${DB_NAME}" -f examples/authors/postgresql/schema.sql + env: + PGPASSWORD: ${{ env.DB_PASS }} + + - name: End-to-End tests + run: bundle exec rake end2end_pg + + end2end-mysql: + name: End-to-End (MySQL) + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:8.3.0 + env: + MYSQL_DATABASE: tests + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -h 127.0.0.1" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + + env: + MYSQL_HOST: 127.0.0.1 + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Create the schema (and allow LOAD DATA LOCAL INFILE for :copyfrom) + run: | + mysql -h 127.0.0.1 -u "${DB_USER}" -e 'SET GLOBAL local_infile = 1' + mysql -h 127.0.0.1 -u "${DB_USER}" "${DB_NAME}" < examples/authors/mysql/schema.sql + + - name: End-to-End tests + run: bundle exec rake end2end_mysql2 + + end2end-sqlite: + name: End-to-End (SQLite) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: End-to-End tests + run: bundle exec rake end2end_sqlite3 diff --git a/.gitignore b/.gitignore index f27d4ef..a7e7b41 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,6 @@ plugin.wasm **/build # generated by rbwasm -plugins \ No newline at end of file +plugins +# scratch output of scripts/tests/run_codegen.rb +.codegen-test* diff --git a/Gemfile b/Gemfile index e934901..6bd5784 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source 'https://rubygems.org' # Gems used by the *generated* code (examples/ + end-to-end tests under tests/). gem 'connection_pool' +gem 'csv' # used by the generated mysql2 code (:copyfrom); a bundled gem since Ruby 3.4 gem 'mysql2' gem 'pg', '1.5.2' gem 'sqlite3' diff --git a/README.md b/README.md index b1d9463..d34034c 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,9 @@ Not applicable to the Ruby plugin (they exist in the C# plugin because of its ru ## Local plugin development ### Prerequisites * Ruby 3.3 and `bundle install` -* [sqlc](https://docs.sqlc.dev/en/latest/overview/install.html) (v1.27.0 was used for verification) +* [sqlc](https://docs.sqlc.dev/en/latest/overview/install.html) (version in [`scripts/tool-versions.env`](scripts/tool-versions.env)) * `curl` + `tar` for building the WASM plugin (it downloads the prebuilt ruby.wasm interpreter) -* Docker + docker-compose, for the end-to-end tests only (they need real MySQL / PostgreSQL servers) +* PostgreSQL / MySQL servers for the end-to-end tests (or Docker, see below) ### Layout | Path | What | @@ -152,34 +152,34 @@ Not applicable to the Ruby plugin (they exist in the C# plugin because of its ru | `lib/sqlc_gen_ruby/` | the generator (`generator.rb`, `method_gen.rb`, `drivers/*.rb`, `options.rb`, ...) | | `test/` | unit + golden tests (`rake test`); `test/fixtures` are real requests captured from sqlc | | `tests/` | end-to-end tests of the generated code against real databases | +| `examples/` | the plugin's output for `examples/authors/*` (kept in sync, CI runs `sqlc diff`) | | `scripts/wasm/` | building the WASM plugin | -### Unit & golden tests +### Everyday commands ``` -bundle exec rake test +bundle exec rake test # unit + golden tests (no database needed) +rake sqlc_generate_process # regenerate examples/ with exe/plugin.rb (needs `ruby` on PATH) +rake sqlc_generate_wasm # build dist/plugin.wasm, then regenerate examples/ with it +bundle exec rake rubocop steep # the generated code must stay RuboCop- and Steep-clean +rake end2end_sqlite3 # end-to-end, no server needed +rake end2end_pg end2end_mysql2 # end-to-end, needs the servers below +rake run_end2end_tests # all of it in Docker (docker-compose) ``` -The golden tests feed the plugin real `GenerateRequest`s captured from sqlc and diff the result against -[examples/](examples) (and against `test/golden`, recorded from the original C# implementation). -To refresh a fixture see `scripts/capture_request.rb`. +The end-to-end tests read `DB_NAME`, `DB_USER`, `DB_PASS`, `POSTGRES_HOST`, `MYSQL_HOST` (and optionally +`POSTGRES_PORT`, `MYSQL_PORT`); load `examples/authors//schema.sql` into the database first. MySQL needs +`local_infile=1` for `:copyfrom`. -### Generating code -SQLC utilizes our process / WASM plugin to generate code -``` -rake sqlc_generate_process # runs exe/plugin.rb directly (needs `ruby` on PATH) -rake sqlc_generate_wasm # builds dist/plugin.wasm first and runs that -``` - -### Testing generated code -Testing the SQLC generated code via a predefined flow: -``` -rake test_process_plugin -rake test_wasm_plugin -``` +The golden tests feed the plugin real `GenerateRequest`s captured from sqlc and compare with [examples/](examples) and +`test/golden` (`UPDATE_GOLDEN=1 ruby -Itest test/extended_golden_test.rb` rewrites the snapshots - review the diff). +To refresh a fixture see `scripts/capture_request.rb`. -### Building the WASM plugin -``` -rake wasm_build # == ./scripts/wasm/build.sh, writes dist/plugin.wasm (~18 MB) -``` +### CI +* **Build** (`build.yml`): builds `plugin.wasm`; lint (Ruby warnings, RuboCop on the generated code); codegen tests - + `sqlc diff` for the WASM *and* the process plugin, Steep, and generation with different options (Ruby 3.1 / 3.2 / 3.3, + types, Gemfile) checked with the matching Ruby. +* **Tests** (`tests.yml`): unit tests, the plugin on Ruby 3.1 / 3.2 / 3.4, and the end-to-end tests against PostgreSQL + and MySQL service containers and SQLite. +* Tool versions live in `scripts/tool-versions.env`; dependabot keeps actions and gems current. ## Release flow The release flow in this repo follows the semver conventions, building tag as `v[major].[minor].[patch]`. diff --git a/Rakefile b/Rakefile index 7c84fef..c9904d4 100755 --- a/Rakefile +++ b/Rakefile @@ -12,6 +12,14 @@ Rake::TestTask.new(:end2end_tests) do |t| # called inside Docker t.pattern = 'tests/end2end_*.rb' end +# one database at a time (CI runs them as separate jobs against service containers) +%w[pg mysql2 sqlite3].each do |driver| + Rake::TestTask.new("end2end_#{driver}") do |t| + t.description = "End-to-end tests of the #{driver} examples against a real database" + t.pattern = "tests/end2end_#{driver}.rb" + end +end + task :rubocop do sh 'bundle exec rubocop examples' end @@ -42,6 +50,11 @@ task :update_wasm_plugin do sh './scripts/wasm/update_sha.sh sqlc.ci.yaml' end +# regenerates docs/Examples.md from sqlc.ci.yaml +task :docs do + sh 'ruby scripts/docs/generate_examples.rb docs/Examples.md' +end + task :sqlc_generate_wasm => [:wasm_build, :update_wasm_plugin] do ENV['SQLCCACHE'] = './' sh 'sqlc -f sqlc.ci.yaml generate' diff --git a/docs/Examples.md b/docs/Examples.md index 6fde112..024acc1 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -34,4 +34,3 @@ rubyVersion: "3.3" generateTypes: true generateGemfile: false ``` - diff --git a/scripts/docs/generate_examples.rb b/scripts/docs/generate_examples.rb new file mode 100644 index 0000000..767838f --- /dev/null +++ b/scripts/docs/generate_examples.rb @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Regenerates docs/Examples.md from the sqlc config (one section per `sql:` block). +# ruby scripts/docs/generate_examples.rb docs/Examples.md +require 'yaml' + +doc_file = ARGV.fetch(0) +config = YAML.safe_load_file(File.expand_path('../../sqlc.ci.yaml', __dir__)) + +def yaml_value(value) = value.is_a?(String) && value.match?(/\A[\d.]+\z/) ? %("#{value}") : value + +sections = config['sql'].map do |sql| + options = sql['codegen'].first['options'] + driver = options['driver'] + yaml = options.map { |key, value| "#{key}: #{yaml_value(value)}" }.join("\n") + [ + "## Engine `#{sql['engine']}`: [examples/#{driver}](../examples/#{driver})", + '', + "### [Schema](../#{sql['schema']}) | [Queries](../#{sql['queries']}) | [End2End Test](../tests/end2end_#{driver}.rb)", + '', + '### Config', + '```yaml', + yaml, + '```', + '' + ].join("\n") +end + +File.write(doc_file, "# Examples\n#{sections.join("\n")}") diff --git a/scripts/docs/generate_examples.sh b/scripts/docs/generate_examples.sh deleted file mode 100755 index d7634fe..0000000 --- a/scripts/docs/generate_examples.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -e - -doc_file=$1 - -examples_cnt=$(yq ".sql | length" sqlc.ci.yaml) -examples_doc="# Examples" - -for ((i = 0 ; i < "${examples_cnt}" ; i++ )); do - engine_name=$(yq ".sql[${i}].engine" sqlc.ci.yaml) - schema_file=$(yq ".sql[${i}].schema" sqlc.ci.yaml) - query_files=$(yq ".sql[${i}].queries" sqlc.ci.yaml) - driver=$(yq ".sql[${i}].codegen[0].options.driver" sqlc.ci.yaml) - test_file="end2end_${driver}.rb" - - examples_doc+=" -## Engine \`${engine_name}\`: [examples/${driver}](../examples/${driver}) - -### [Schema](../${schema_file}) | [Queries](../${query_files}) | [End2End Test](../tests/${test_file}) - -### Config -\`\`\`yaml -$(yq ".sql[${i}].codegen[${0}].options" sqlc.ci.yaml) -\`\`\` -" -done - -echo "${examples_doc}" > "${doc_file}" \ No newline at end of file diff --git a/scripts/tests/run_codegen.rb b/scripts/tests/run_codegen.rb new file mode 100755 index 0000000..6460890 --- /dev/null +++ b/scripts/tests/run_codegen.rb @@ -0,0 +1,73 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Runs `sqlc generate` with a sqlc config for different plugin options and checks what comes out, without touching +# the checked-in examples (they are verified separately with `sqlc diff`). +# +# scripts/tests/run_codegen.rb +# +# Checks, for every `sql:` block of the config: +# * every generated .rb file is valid Ruby (for the Ruby running this script) and models/connection files load +# * .rbs files exist iff generateTypes, a Gemfile exists iff generateGemfile +require 'fileutils' +require 'yaml' + +config_path, ruby_version, generate_types, generate_gemfile = ARGV +abort "usage: #{$PROGRAM_NAME} " unless generate_gemfile + +types = generate_types == 'true' +gemfile = generate_gemfile == 'true' +config = YAML.safe_load_file(config_path) + +# sqlc resolves `out:` relative to the config file, so the scratch directory lives next to it (gitignored) +config_dir = File.dirname(config_path) +scratch = ".codegen-test-out-#{Process.pid}" +out_root = File.join(config_dir, scratch) + +begin + config['sql'].each do |sql| + codegen = sql['codegen'].first + options = codegen['options'] + codegen['out'] = File.join(scratch, options['driver']) + options.merge!('rubyVersion' => ruby_version, 'generateTypes' => types, 'generateGemfile' => gemfile) + puts "#{options['driver']}: #{options}" + end + + # sqlc resolves paths relative to the config file: write the temporary config next to the original + tmp_config = File.join(config_dir, ".codegen-test.#{Process.pid}.yaml") + begin + File.write(tmp_config, config.to_yaml) + system('sqlc', '-f', tmp_config, 'generate') or abort('sqlc generate failed') + ensure + FileUtils.rm_f(tmp_config) + end + + failures = [] + config['sql'].each do |sql| + driver = sql['codegen'].first['options']['driver'] + dir = File.join(out_root, driver) + files = Dir.children(dir).sort + puts "#{driver}: #{files.join(', ')}" + + failures << "#{driver}: no query_sql.rb" unless files.include?('query_sql.rb') + failures << "#{driver}: .rbs files #{types ? 'missing' : 'generated although generateTypes is false'}" if files.grep(/\.rbs\z/).empty? == types + failures << "#{driver}: Gemfile #{gemfile ? 'missing' : 'generated although generateGemfile is false'}" if files.include?('Gemfile') != gemfile + + files.grep(/\.rb\z/).each do |file| + path = File.join(dir, file) + failures << "#{driver}/#{file} is not valid Ruby" unless system(RbConfig.ruby, '-c', path, out: File::NULL) + end + # models.rb / connection.rb have no dependencies: they must actually load (Data / Struct classes are valid) + %w[connection.rb models.rb].each do |file| + path = File.join(dir, file) + next unless File.exist?(path) + + failures << "#{driver}/#{file} does not load" unless system(RbConfig.ruby, '-e', "require 'set'; load ARGV[0]", path) + end + end + + abort(failures.map { |f| "FAIL #{f}" }.join("\n")) unless failures.empty? + puts 'codegen checks passed' +ensure + FileUtils.rm_rf(out_root) +end diff --git a/scripts/tests/run_codegen.sh b/scripts/tests/run_codegen.sh deleted file mode 100755 index 8acb6fd..0000000 --- a/scripts/tests/run_codegen.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -e -source .env - -mapfile -t examples < <(find examples -name "*.rb" -exec dirname {} +) - -config_file=$1 -ruby_version=$2 -generate_types=$3 -generate_gemfile=$4 - -generated_files_cleanup() { - for example_dir in "${examples[@]}" - do - echo "Deleting .rb & .rbs files in ${example_dir}" - find "${example_dir}/" -type f -name "*.rb" -exec rm -f {} \; - find "${example_dir}/" -type f -name "*.rbs" -exec rm -f {} \; - if [ "${generate_gemfile}" = "true" ]; then - echo "Deleting Gemfile file" && rm "${example_dir}/Gemfile" - fi - done -} - -change_config() { - for ((i=0; i<${#examples[@]}; i++)); do - echo "Changing configuration for project ${example_dir}" - yq -i " - .sql[${i}].codegen[0].options.rubyVersion = \"${ruby_version}\" | - .sql[${i}].codegen[0].options.generateTypes = ${generate_types} | - .sql[${i}].codegen[0].options.generateGemfile = ${generate_gemfile} - " "${config_file}" - echo "${examples[i]} codegen config:" && yq ".sql[${i}].codegen[0]" "${config_file}" - done -} - -#check_gemfile() { -# for example_dir in "${examples[@]}" -# do -# echo "Checking Gemfile generated" -# if [ ! -f "${example_dir}/Gemfile" ]; then -# echo "Assertion failed: A Gemfile is not present in the directory ${example_dir}." -# return 1 -# fi -# done -#} - -#check_project_compiles() { -# if [ "${generate_csproj}" = "true" ]; then -# for example_dir in "${examples[@]}" -# do -# echo "Checking ${example_dir} project compiles" -# dotnet build "${example_dir}/" -# done -# fi -#} - -generated_files_cleanup && change_config -sqlc -f "${config_file}" generate - -#test_functions=("check_csproj_file" "check_project_compiles") -#for test_function in "${test_functions[@]}"; do -# ${test_function} -# status_code=$? -# if [ ${status_code} -ne 0 ]; then -# echo "Function ${test_function} failed with status code ${status_code}" -# exit "${status_code}" -# fi -# echo "Test ${test_function} passed" -#done \ No newline at end of file diff --git a/scripts/tests/run_end2end.sh b/scripts/tests/run_end2end.sh index 099baf5..f67210a 100755 --- a/scripts/tests/run_end2end.sh +++ b/scripts/tests/run_end2end.sh @@ -1,24 +1,17 @@ #!/usr/bin/env bash +# Local end-to-end run: starts MySQL + PostgreSQL with docker-compose and runs tests/end2end_*.rb in a container. +# (CI runs the same tests against service containers, see .github/workflows/tests.yml.) +set -ex -if [ "$GITHUB_ACTIONS" = "true" ]; then - echo "Running in Github Actions" - tests_container_id=$(docker ps -aqf "name=${TESTS_CONTAINER_NAME}") - tests_exit_code=$(docker wait "${TESTS_CONTAINER_NAME}") - docker logs --timestamps "${tests_container_id}" -else - echo "Running locally" - set -ex +destroy() { docker-compose down --volumes; } +trap destroy EXIT - destroy() { docker-compose down --volumes; } - trap destroy EXIT +source .env +docker-compose up --detach --build --force-recreate --remove-orphans +tests_container_id=$(docker ps -aqf "name=${TESTS_CONTAINER_NAME}") +tests_exit_code=$(docker wait "${TESTS_CONTAINER_NAME}") - source .env - docker-compose up --detach --build --force-recreate --remove-orphans - tests_container_id=$(docker ps -aqf "name=${TESTS_CONTAINER_NAME}") - tests_exit_code=$(docker wait "${TESTS_CONTAINER_NAME}") - - # by container_id since the container is dead - docker logs --timestamps "${tests_container_id}" > tests.log && cat tests.log -fi +# by container_id since the container is dead +docker logs --timestamps "${tests_container_id}" > tests.log && cat tests.log exit "${tests_exit_code}" diff --git a/scripts/tool-versions.env b/scripts/tool-versions.env new file mode 100644 index 0000000..3a011af --- /dev/null +++ b/scripts/tool-versions.env @@ -0,0 +1,5 @@ +# Versions of the external tools used by the build and CI (sourced by scripts/wasm/build.sh and the workflows). +# The prebuilt ruby.wasm interpreter embedded in dist/plugin.wasm (https://github.com/ruby/ruby.wasm/releases) +RUBY_WASM_VERSION=2.7.1 +# sqlc used to generate / diff the examples and to run the plugin +SQLC_VERSION=1.27.0 diff --git a/scripts/wasm/build.sh b/scripts/wasm/build.sh index 08419c1..bca558b 100755 --- a/scripts/wasm/build.sh +++ b/scripts/wasm/build.sh @@ -3,7 +3,9 @@ # Needs: ruby + `gem install ruby_wasm` (for `rbwasm pack`), curl, tar. set -euo pipefail cd "$(dirname "$0")/../.." -RUBY_WASM_VERSION="${RUBY_WASM_VERSION:-2.7.1}" +# shellcheck source=../tool-versions.env +source scripts/tool-versions.env +RUBY_WASM_VERSION="${RUBY_WASM_VERSION}" FLAVOR="ruby-3.3-wasm32-unknown-wasip1-full" # `rbwasm` comes from the ruby_wasm gem (see Gemfile, group :wasm_build) if [ -z "${RBWASM:-}" ]; then diff --git a/scripts/wasm/update_sha.sh b/scripts/wasm/update_sha.sh index 8dd62f7..0c4d2a7 100755 --- a/scripts/wasm/update_sha.sh +++ b/scripts/wasm/update_sha.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# Writes the sha256 of dist/plugin.wasm into the sqlc config (plugins[].wasm.sha256) - idempotent. +set -euo pipefail -filename=$1 +filename=${1:?usage: update_sha.sh } plugin_sha=$(shasum -a 256 dist/plugin.wasm | awk '{ print $1 }') -yq -i ".plugins[0].wasm.sha256 = \"${plugin_sha}\"" "${filename}" \ No newline at end of file +sed -i.bak -E "s/^([[:space:]]*sha256:[[:space:]]*).*/\1${plugin_sha}/" "${filename}" && rm -f "${filename}.bak" +grep -q "${plugin_sha}" "${filename}" || { echo "no sha256: line in ${filename}" >&2; exit 1; } From ec2a88b250b90f8a7836281b5bd4d93306005739 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:44:10 +0000 Subject: [PATCH 12/15] Lock the csv gem added to the Gemfile (setup-ruby installs in frozen mode) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index df8e044..f8c41ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -120,6 +120,7 @@ PLATFORMS DEPENDENCIES async connection_pool + csv google-protobuf (~> 4.0) minitest mysql2 From dc48acfca66094ab235bb2add05dfc8206a45b47 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:45:15 +0000 Subject: [PATCH 13/15] CI: load only KEY=value lines of tool-versions.env into GITHUB_ENV; skip the Data test before Ruby 3.2 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .github/workflows/build.yml | 4 ++-- test/features_test.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f4d526..4bedf59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v7 - name: Load tool versions - run: cat scripts/tool-versions.env >> "$GITHUB_ENV" + run: grep -E '^[A-Z_]+=' scripts/tool-versions.env >> "$GITHUB_ENV" - uses: ruby/setup-ruby@v1 with: @@ -121,7 +121,7 @@ jobs: - uses: actions/checkout@v7 - name: Load tool versions - run: cat scripts/tool-versions.env >> "$GITHUB_ENV" + run: grep -E '^[A-Z_]+=' scripts/tool-versions.env >> "$GITHUB_ENV" # the generated code for rubyVersion X is checked with a Ruby X interpreter - uses: ruby/setup-ruby@v1 diff --git a/test/features_test.rb b/test/features_test.rb index 863eb60..1ef0c87 100644 --- a/test/features_test.rb +++ b/test/features_test.rb @@ -208,6 +208,7 @@ def test_struct_classes_are_valid_and_usable end def test_data_classes_are_usable + skip 'Data needs Ruby 3.2' unless defined?(Data) files = generate(load_request('pg')) scope = Module.new scope.module_eval(files.fetch('models.rb')) From 4f16647c2c9e68f4f2cbb48b325356f33ee9734b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:49:53 +0000 Subject: [PATCH 14/15] Add Ruby conventions to the generated code: bang finders and logger support - every :one query also gets `get_author!(...)`, which raises QuerySql::NotFound instead of returning nil (the find_by / find! convention); RBS declares the non-nil row type - `logger:` on `new` / `connect` (anything with #debug: Logger, Rails.logger) logs "QuerySql#get_author (0.4ms)" for every query and transaction; parameters are never logged - all drivers acquire the connection through `with_client(name)`; SQLite's transaction is instrumented too Tests: unit tests for the generated code, end-to-end tests on PostgreSQL, MySQL and SQLite. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- README.md | 2 + examples/mysql2/query_sql.rb | 95 ++-- examples/mysql2/query_sql.rbs | 17 +- examples/pg/query_sql.rb | 109 +++-- examples/pg/query_sql.rbs | 21 +- examples/sqlite3/query_sql.rb | 338 ++++++++------ examples/sqlite3/query_sql.rbs | 11 +- lib/sqlc_gen_ruby/drivers/base.rb | 28 +- lib/sqlc_gen_ruby/drivers/mysql2.rb | 36 +- lib/sqlc_gen_ruby/drivers/pg.rb | 37 +- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 45 +- lib/sqlc_gen_ruby/method_gen.rb | 23 +- steep_expectations.yml | 40 +- test/features_test.rb | 23 + test/golden/mysql2/default/my_queries_sql.rb | 40 +- test/golden/mysql2/default/my_queries_sql.rbs | 11 +- test/golden/mysql2/default/other_sql.rb | 49 +- test/golden/mysql2/default/other_sql.rbs | 13 +- test/golden/mysql2/default/query_sql.rb | 106 +++-- test/golden/mysql2/default/query_sql.rbs | 19 +- .../mysql2/ruby31_gemfile/my_queries_sql.rb | 40 +- .../mysql2/ruby31_gemfile/my_queries_sql.rbs | 11 +- .../golden/mysql2/ruby31_gemfile/other_sql.rb | 49 +- .../mysql2/ruby31_gemfile/other_sql.rbs | 13 +- .../golden/mysql2/ruby31_gemfile/query_sql.rb | 106 +++-- .../mysql2/ruby31_gemfile/query_sql.rbs | 19 +- .../mysql2/ruby32_notypes/my_queries_sql.rb | 40 +- .../golden/mysql2/ruby32_notypes/other_sql.rb | 49 +- .../golden/mysql2/ruby32_notypes/query_sql.rb | 106 +++-- test/golden/pg/default/my_queries_sql.rb | 40 +- test/golden/pg/default/my_queries_sql.rbs | 11 +- test/golden/pg/default/other_sql.rb | 49 +- test/golden/pg/default/other_sql.rbs | 13 +- test/golden/pg/default/query_sql.rb | 120 +++-- test/golden/pg/default/query_sql.rbs | 23 +- .../pg/ruby31_gemfile/my_queries_sql.rb | 40 +- .../pg/ruby31_gemfile/my_queries_sql.rbs | 11 +- test/golden/pg/ruby31_gemfile/other_sql.rb | 49 +- test/golden/pg/ruby31_gemfile/other_sql.rbs | 13 +- test/golden/pg/ruby31_gemfile/query_sql.rb | 120 +++-- test/golden/pg/ruby31_gemfile/query_sql.rbs | 23 +- .../pg/ruby32_notypes/my_queries_sql.rb | 40 +- test/golden/pg/ruby32_notypes/other_sql.rb | 49 +- test/golden/pg/ruby32_notypes/query_sql.rb | 120 +++-- test/golden/sqlite3/default/my_queries_sql.rb | 62 ++- .../golden/sqlite3/default/my_queries_sql.rbs | 11 +- test/golden/sqlite3/default/other_sql.rb | 113 +++-- test/golden/sqlite3/default/other_sql.rbs | 13 +- test/golden/sqlite3/default/query_sql.rb | 422 ++++++++++-------- test/golden/sqlite3/default/query_sql.rbs | 13 +- .../sqlite3/ruby31_gemfile/my_queries_sql.rb | 62 ++- .../sqlite3/ruby31_gemfile/my_queries_sql.rbs | 11 +- .../sqlite3/ruby31_gemfile/other_sql.rb | 113 +++-- .../sqlite3/ruby31_gemfile/other_sql.rbs | 13 +- .../sqlite3/ruby31_gemfile/query_sql.rb | 422 ++++++++++-------- .../sqlite3/ruby31_gemfile/query_sql.rbs | 13 +- .../sqlite3/ruby32_notypes/my_queries_sql.rb | 62 ++- .../sqlite3/ruby32_notypes/other_sql.rb | 113 +++-- .../sqlite3/ruby32_notypes/query_sql.rb | 422 ++++++++++-------- tests/end2end_mysql2.rb | 22 + tests/end2end_pg.rb | 22 + tests/end2end_sqlite3.rb | 22 + 62 files changed, 2752 insertions(+), 1396 deletions(-) diff --git a/README.md b/README.md index d34034c..48342fd 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ queries.transaction(isolation: :serializable) do |tx| # COMMIT on return, R end ``` * **Keyword arguments:** every query parameter is a keyword argument (missing / unknown ones raise `ArgumentError`). Parameters named with `sqlc.arg` / `sqlc.narg` that are nullable default to `nil`; a plain nullable column (`INSERT ... VALUES ($1, $2)`) has to be passed explicitly. Names are snake_cased, Ruby keywords get a trailing underscore (`class_:`). +* **Bang finders:** every `:one` query also gets a `get_author!` that raises `QuerySql::NotFound` instead of returning `nil` (like `find_by` vs `find`). +* **Logging:** `QuerySql.connect(params, logger: Rails.logger)` / `QuerySql.new(pool, logger: logger)` logs `QuerySql#get_author (0.4ms)` at debug level for every query and transaction (SQL parameters are never logged). Without a logger there is no overhead. * **Rows** are immutable `Data` objects (`Struct` for `rubyVersion` below 3.2), so they work with pattern matching: `case row in { id:, name: }`. * **Connections:** `QuerySql.connect(params, **pool_options)` builds a [`connection_pool`](https://github.com/mperham/connection_pool); to share a pool you already have (or to inject a fake in tests) use `QuerySql.new(pool)` - anything that responds to `#with` and yields a connection. For SQLite pass a `SQLite3::Database` to `QuerySql.new(db)`. * **Async / fibers:** the generated code is plain synchronous Ruby and works with the Fiber scheduler (the [`async`](https://github.com/socketry/async) gem): inside `Sync { ... }` pooled queries on `pg` and `mysql2` overlap instead of blocking (see `tests/end2end_pg.rb`). SQLite calls are synchronous C calls and do not overlap. diff --git a/examples/mysql2/query_sql.rb b/examples/mysql2/query_sql.rb index 44d849b..2382175 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -189,20 +189,26 @@ class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type, SQL class QuerySql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -227,7 +233,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] stmt = client.prepare(GET_AUTHOR_SQL) begin @@ -240,8 +246,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| stmt = client.prepare(LIST_AUTHORS_SQL) begin result = stmt.execute @@ -255,7 +266,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_SQL) begin @@ -268,7 +279,7 @@ def create_author(name:, bio:) end def update_author(bio:, id:) - with_client do |client| + with_client('update_author') do |client| query_params = [bio, id] stmt = client.prepare(UPDATE_AUTHOR_SQL) begin @@ -281,7 +292,7 @@ def update_author(bio:, id:) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) begin @@ -294,7 +305,7 @@ def create_author_return_id(name:, bio:) end def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] stmt = client.prepare(DELETE_AUTHOR_SQL) begin @@ -307,7 +318,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| stmt = client.prepare(TEST_SQL) begin result = stmt.execute @@ -357,8 +368,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(limit:, offset:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [limit, offset] stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) begin @@ -373,7 +389,7 @@ def list_authors_paged(limit:, offset:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) begin @@ -389,7 +405,7 @@ def get_author_by_name_pattern(name_pattern: nil) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio: nil) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) begin @@ -402,7 +418,7 @@ def update_author_bios(bio: nil) end def get_authors_by_ids(ids:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [*ids] sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) stmt = client.prepare(sql) @@ -418,7 +434,7 @@ def get_authors_by_ids(ids:) end def get_authors_by_ids_and_names(ids:, names:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [*ids, *names] sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) @@ -436,7 +452,7 @@ def get_authors_by_ids_and_names(ids:, names:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| return if rows.empty? Tempfile.create(['copy_authors', '.csv']) do |file| @@ -451,7 +467,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| stmt = client.prepare(TRUNCATE_AUTHORS_SQL) begin stmt.execute @@ -463,7 +479,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] stmt = client.prepare(CREATE_BOOK_SQL) begin @@ -476,7 +492,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) begin result = stmt.execute(as: :array) @@ -490,7 +506,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) begin result = stmt.execute(as: :array) @@ -504,7 +520,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) begin @@ -519,7 +535,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:, author_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type, author_type] stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) begin @@ -532,7 +548,7 @@ def create_extended_bio(author_name:, name:, bio_type:, author_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) begin @@ -545,8 +561,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) begin stmt.execute @@ -559,10 +580,24 @@ def truncate_extended_bios private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index 82b68e0..df71556 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -196,14 +196,19 @@ module Mysql2Codegen TRUNCATE_EXTENDED_BIOS_SQL: String class QuerySql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -216,6 +221,8 @@ module Mysql2Codegen def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -242,11 +249,15 @@ module Mysql2Codegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def csv_line: (Array[untyped] values) -> String diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index cc23eeb..9849d78 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -204,20 +204,26 @@ class GetExtraTypesRow < Data.define( SQL class QuerySql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -238,7 +244,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_get_author') client.prepare('query_sql_get_author', GET_AUTHOR_SQL) @@ -250,8 +256,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| unless client.prepared_statements.include?('query_sql_list_authors') client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) client.prepared_statements.add('query_sql_list_authors') @@ -264,7 +275,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author') client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) @@ -276,8 +287,13 @@ def create_author(name:, bio:) end end + # Like #create_author, but raises NotFound when there is no row. + def create_author!(...) + create_author(...) || raise(NotFound, 'CreateAuthor returned no row') + end + def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_delete_author') client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) @@ -289,7 +305,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| unless client.prepared_statements.include?('query_sql_test') client.prepare('query_sql_test', TEST_SQL) client.prepared_statements.add('query_sql_test') @@ -319,8 +335,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(offset:, limit:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [offset, limit] unless client.prepared_statements.include?('query_sql_list_authors_paged') client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) @@ -334,7 +355,7 @@ def list_authors_paged(offset:, limit:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) @@ -348,7 +369,7 @@ def get_author_by_name_pattern(name_pattern: nil) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author_return_id') client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) @@ -361,7 +382,7 @@ def create_author_return_id(name:, bio:) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] unless client.prepared_statements.include?('query_sql_update_author_bios') client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) @@ -373,7 +394,7 @@ def update_author_bios(bio:) end def get_authors_by_ids(param1:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [param1] unless client.prepared_statements.include?('query_sql_get_authors_by_ids') client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) @@ -387,7 +408,7 @@ def get_authors_by_ids(param1:) end def get_authors_by_ids_and_names(param1:, param2:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [param1, param2] unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) @@ -402,7 +423,7 @@ def get_authors_by_ids_and_names(param1:, param2:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do rows.each do |row| @@ -414,7 +435,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| unless client.prepared_statements.include?('query_sql_truncate_authors') client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_truncate_authors') @@ -425,7 +446,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] unless client.prepared_statements.include?('query_sql_create_book') client.prepare('query_sql_create_book', CREATE_BOOK_SQL) @@ -437,7 +458,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| unless client.prepared_statements.include?('query_sql_list_all_authors_books') client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) client.prepared_statements.add('query_sql_list_all_authors_books') @@ -450,7 +471,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| unless client.prepared_statements.include?('query_sql_get_duplicate_authors') client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_get_duplicate_authors') @@ -463,7 +484,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) @@ -477,7 +498,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type] unless client.prepared_statements.include?('query_sql_create_extended_bio') client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) @@ -489,7 +510,7 @@ def create_extended_bio(author_name:, name:, bio_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) @@ -501,8 +522,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| unless client.prepared_statements.include?('query_sql_truncate_extended_bios') client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) client.prepared_statements.add('query_sql_truncate_extended_bios') @@ -513,7 +539,7 @@ def truncate_extended_bios end def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) - with_client do |client| + with_client('insert_extra_types') do |client| query_params = [ c_uuid, c_jsonb, @@ -534,7 +560,7 @@ def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_tim end def get_extra_types - with_client do |client| + with_client('get_extra_types') do |client| unless client.prepared_statements.include?('query_sql_get_extra_types') client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_get_extra_types') @@ -554,8 +580,13 @@ def get_extra_types end end + # Like #get_extra_types, but raises NotFound when there is no row. + def get_extra_types!(...) + get_extra_types(...) || raise(NotFound, 'GetExtraTypes returned no row') + end + def truncate_extra_types - with_client do |client| + with_client('truncate_extra_types') do |client| unless client.prepared_statements.include?('query_sql_truncate_extra_types') client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_truncate_extra_types') @@ -567,10 +598,24 @@ def truncate_extra_types private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index fe3e8a5..7c71e61 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -199,22 +199,31 @@ module PgCodegen TRUNCATE_EXTRA_TYPES_SQL: String class QuerySql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> CreateAuthorRow? + def create_author!: (name: String, bio: String?) -> CreateAuthorRow + def delete_author: (id: Integer) -> void def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -243,17 +252,23 @@ module PgCodegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void def get_extra_types: -> GetExtraTypesRow? + def get_extra_types!: -> GetExtraTypesRow + def truncate_extra_types: -> void private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index 1e87e12..109053c 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -128,239 +128,283 @@ class GetAuthorsByBookNameRow < Data.define(:id, :name, :bio, :book) end class QuerySql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end # Returns the author with the given id, or nil when there is none. def get_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(GET_AUTHOR_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && GetAuthorRow.new(row[0], row[1], row[2]) - ensure - stmt.close + with_client('get_author') do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - client = @db - stmt = client.prepare(LIST_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAuthorsRow.new(row[0], row[1], row[2]) + with_client('list_authors') do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('create_author') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def delete_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(DELETE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('delete_author') do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def list_authors_paged(offset:, limit:) - client = @db - query_params = [offset, limit] - stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - ListAuthorsPagedRow.new(row[0], row[1], row[2]) + with_client('list_authors_paged') do |client| + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_author_by_name_pattern(name_pattern: nil) - client = @db - query_params = [name_pattern] - stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + with_client('get_author_by_name_pattern') do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author_return_id(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_author_return_id') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - client = @db - query_params = [bio] - stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) - begin - stmt.execute(*query_params) - client.changes - ensure - stmt.close + with_client('update_author_bios') do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end end end def get_authors_by_ids(ids:) - client = @db - query_params = [*ids] - sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids') do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_ids_and_names(ids:, names:) - client = @db - query_params = [*ids, *names] - sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids_and_names') do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - client = @db - rows.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + with_client('copy_authors') do |client| + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil end - nil end def truncate_authors - client = @db - stmt = client.prepare(TRUNCATE_AUTHORS_SQL) - begin - stmt.execute - nil - ensure - stmt.close + with_client('truncate_authors') do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end end end def create_book(name:, author_id:) - client = @db - query_params = [name, author_id] - stmt = client.prepare(CREATE_BOOK_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_book') do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end def list_all_authors_books - client = @db - stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + with_client('list_all_authors_books') do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_duplicate_authors - client = @db - stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + with_client('get_duplicate_authors') do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_book_name(name:) - client = @db - query_params = [name] - stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + with_client('get_authors_by_book_name') do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end private + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end + def slice_placeholders(values) values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') end diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index 4ec86e0..96a1a35 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -109,12 +109,17 @@ module Sqlite3Codegen end class QuerySql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -147,6 +152,10 @@ module Sqlite3Codegen private + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T + def slice_placeholders: (Array[untyped] values) -> String end end diff --git a/lib/sqlc_gen_ruby/drivers/base.rb b/lib/sqlc_gen_ruby/drivers/base.rb index 6784946..58ea322 100644 --- a/lib/sqlc_gen_ruby/drivers/base.rb +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -39,7 +39,7 @@ def class_methods = raise(NotImplementedError) def private_methods = [] # How a query method acquires the database connection (yielding `client`). - def client_block = '@db.with do |client|' + def client_block(func) = "with_client('#{func}') do |client|" # RHS of the generated `Sql` constant (without the constant name). def query_text(query) = query.text @@ -50,10 +50,6 @@ def prepare_stmt(_stmt_name, _const) = raise(NotImplementedError) # array_rows: rows are read by position (needed when column names are ambiguous, e.g. sqlc.embed). def execute_stmt(_stmt_name, _const, _has_params, array_rows: false) = raise(NotImplementedError) - # Whether generated methods borrow the client from a ConnectionPool (`@db.with do |client|`) - # or use the single injected connection (`client = @db`). - def pooling_enabled? = true - # Whether the prepared `stmt` must be closed after use (mysql2 / sqlite3 prepare on every call; unclosed # statements leak server resources and make SQLite refuse to COMMIT). def closes_statements? = false @@ -111,6 +107,28 @@ def query_text_constant(query) }.freeze RUBY + # Logs "QuerySql#get_author (0.4ms)" at debug level when the object was created with `logger:`. + INSTRUMENT = <<~'RUBY'.chomp + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + RUBY + + INSTRUMENT_RBS = 'def instrument: [T] (String name) { () -> T } -> T' + + NOT_FOUND = <<~'RUBY'.chomp + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + RUBY + ISOLATION_RBS = 'ISOLATION_LEVELS: Hash[Symbol, String]' TRANSACTION_RBS = 'def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | ' \ diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb index 3bac709..eaac2a4 100644 --- a/lib/sqlc_gen_ruby/drivers/mysql2.rb +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -17,18 +17,17 @@ def module_members def class_methods [ - [CONNECT, 'def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance'], - ["def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [NOT_FOUND, "class NotFound < StandardError\nend"], + [CONNECT, 'def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance'], + [INITIALIZE, 'def initialize: (untyped db, ?logger: untyped) -> void'], [TRANSACTION, TRANSACTION_RBS] ] end def private_methods - [[WITH_CLIENT, 'def with_client: [T] () { (untyped) -> T } -> T']] + [[INSTRUMENT, INSTRUMENT_RBS], [WITH_CLIENT, 'def with_client: [T] (String name) { (untyped) -> T } -> T']] end - def client_block = 'with_client do |client|' - def prepare_stmt(_stmt_name, const) = "stmt = client.prepare(#{const})" def execute_stmt(_stmt_name, _const, has_params, array_rows: false) @@ -77,11 +76,20 @@ module ConnectionState end RUBY + INITIALIZE = <<~'RUBY'.chomp + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) + @db = db + @logger = logger + end + RUBY + CONNECT = <<~'RUBY'.chomp - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end RUBY @@ -89,7 +97,7 @@ def self.connect(mysql2_params, **pool_options) # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -114,10 +122,12 @@ def transaction(isolation: nil) RUBY WITH_CLIENT = <<~'RUBY'.chomp - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end RUBY diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index 7f8235a..954cad9 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -22,21 +22,21 @@ def module_members def class_methods [ - [CONNECT, 'def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance'], - ["def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [NOT_FOUND, "class NotFound < StandardError\nend"], + [CONNECT, 'def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance'], + [INITIALIZE, 'def initialize: (untyped db, ?logger: untyped) -> void'], [TRANSACTION, TRANSACTION_RBS] ] end def private_methods [ - [WITH_CLIENT, 'def with_client: [T] () { (untyped) -> T } -> T'], + [INSTRUMENT, INSTRUMENT_RBS], + [WITH_CLIENT, 'def with_client: [T] (String name) { (untyped) -> T } -> T'], [CONFIGURE, 'def configure: (untyped client) -> void'] ] end - def client_block = 'with_client do |client|' - # Set is only autoloaded since Ruby 3.2 def extra_requires(_queries, ruby_version) = ruby_version.immutable_data_supported? ? [] : %w[set] @@ -89,11 +89,20 @@ def prepared_statements end RUBY + INITIALIZE = <<~'RUBY'.chomp + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) + @db = db + @logger = logger + end + RUBY + CONNECT = <<~'RUBY'.chomp - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end RUBY @@ -101,7 +110,7 @@ def self.connect(pg_params, **pool_options) # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -122,10 +131,12 @@ def transaction(isolation: nil) RUBY WITH_CLIENT = <<~'RUBY'.chomp - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end RUBY diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb index a6f949f..0c34686 100644 --- a/lib/sqlc_gen_ruby/drivers/sqlite3.rb +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -14,20 +14,26 @@ def name = 'Sqlite3' def class_methods [ - ["# @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads)\n" \ - "def initialize(db)\n @db = db\nend", 'def initialize: (untyped db) -> void'], + [NOT_FOUND, "class NotFound < StandardError\nend"], + [INITIALIZE, 'def initialize: (untyped db, ?logger: untyped) -> void'], [TRANSACTION, "def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T"] ] end + def private_methods + [ + [INSTRUMENT, INSTRUMENT_RBS], + ["def with_client(name, &block)\n instrument(name) { block.call(@db) }\nend", + 'def with_client: [T] (String name) { (untyped) -> T } -> T'] + ] + end + def prepare_stmt(_stmt_name, const) = "stmt = client.prepare(#{const})" def execute_stmt(_stmt_name, _const, has_params, array_rows: false) has_params ? 'stmt.execute(*query_params)' : 'stmt.execute' end - def pooling_enabled? = false - def row_data_type = :array def closes_statements? = true @@ -56,20 +62,31 @@ def copy_from_lines(rows_var, table, columns, attrs, _label) [Code.block("#{rows_var}.each_slice(#{rows_per_batch}) do |batch|", body), 'nil'] end + INITIALIZE = <<~'RUBY'.chomp + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) + @db = db + @logger = logger + end + RUBY + TRANSACTION = <<~'RUBY'.chomp # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end RUBY diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb index 7ba1380..f4691fd 100644 --- a/lib/sqlc_gen_ruby/method_gen.rb +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -43,8 +43,15 @@ def code(query) signature = params.empty? ? '' : "(#{params.map { |p| p.optional ? "#{p.name}: nil" : "#{p.name}:" }.join(', ')})" end doc = query.comments.map { |c| "# #{c.strip}".rstrip }.join("\n") - method = Code.block("def #{func}#{signature}", method_body(statements)) - doc.empty? ? method : "#{doc}\n#{method}" + method = Code.block("def #{func}#{signature}", method_body(func, statements)) + method = "#{doc}\n#{method}" unless doc.empty? + query.cmd == ':one' ? "#{method}\n\n#{bang_method(query, func)}" : method + end + + # get_author! raises NotFound instead of returning nil (like find_by vs find!) + def bang_method(query, func) + Code.block("# Like ##{func}, but raises NotFound when there is no row.\ndef #{func}!(...)", + "#{func}(...) || raise(NotFound, '#{query.name} returned no row')") end def type(query) @@ -55,7 +62,10 @@ def type(query) list = params(query).map { |p| "#{p.optional ? '?' : ''}#{p.name}: #{p.type}" } signature = list.empty? ? '' : "(#{list.join(', ')})" end - "def #{func}: #{signature}#{signature.empty? ? '' : ' '}-> #{return_type(query)}" + rbs = "def #{func}: #{signature}#{signature.empty? ? '' : ' '}-> #{return_type(query)}" + return rbs unless query.cmd == ':one' + + "#{rbs}\n\ndef #{func}!: #{signature}#{signature.empty? ? '' : ' '}-> #{query.name}Row" end # [[code, rbs]] of private helper methods the generated class needs for these queries. @@ -146,11 +156,8 @@ def command_statements(query, stmt_name, const, has_params) end end - def method_body(statements) - body = statements.join("\n") - return Code.block(@driver.client_block, body) if @driver.pooling_enabled? - - "client = @db\n#{body}" + def method_body(func, statements) + Code.block(@driver.client_block(func), statements.join("\n")) end def new_row(query) diff --git a/steep_expectations.yml b/steep_expectations.yml index b98524e..cb894d1 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,30 +3,30 @@ diagnostics: - range: start: - line: 195 + line: 198 character: 10 end: - line: 195 + line: 198 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 195 + line: 198 character: 47 end: - line: 195 + line: 198 character: 53 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 580 + line: 615 character: 6 end: - line: 580 + line: 615 character: 9 severity: WARNING message: 'Cannot find the declaration of constant: `CSV`' @@ -35,70 +35,70 @@ diagnostics: - range: start: - line: 210 + line: 213 character: 10 end: - line: 210 + line: 213 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 210 + line: 213 character: 47 end: - line: 210 + line: 213 character: 49 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 221 + line: 227 character: 49 end: - line: 221 + line: 227 character: 51 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 406 + line: 427 character: 18 end: - line: 406 + line: 427 character: 20 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 579 + line: 624 character: 17 end: - line: 579 + line: 624 character: 19 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 580 + line: 625 character: 34 end: - line: 580 + line: 625 character: 36 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 582 + line: 627 character: 36 end: - line: 582 + line: 627 character: 38 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' diff --git a/test/features_test.rb b/test/features_test.rb index 1ef0c87..c6753f0 100644 --- a/test/features_test.rb +++ b/test/features_test.rb @@ -216,4 +216,27 @@ def test_data_classes_are_usable assert_equal 'a', author.name assert author.frozen? end + + # ---- Ruby conventions ---------------------------------------------------------------------------------------- + + def test_bang_finders_for_one_queries + %w[pg mysql2 sqlite3].each do |driver| + files = generate(load_request(driver)) + assert_includes files.fetch('query_sql.rb'), "def get_author!(...)\n get_author(...) || raise(NotFound, 'GetAuthor returned no row')" + assert_includes files.fetch('query_sql.rb'), 'class NotFound < StandardError' + assert_includes files.fetch('query_sql.rbs'), 'def get_author!: (id: Integer) -> GetAuthorRow' + refute_includes files.fetch('query_sql.rb'), 'def list_authors!' # only :one queries + end + end + + def test_logger_support + %w[pg mysql2 sqlite3].each do |driver| + rb = generate(load_request(driver)).fetch('query_sql.rb') + assert_includes rb, 'def initialize(db, logger: nil)' + assert_includes rb, "with_client('get_author') do |client|" + assert_includes rb, 'def instrument(name)' + assert_includes generate(load_request(driver)).fetch('query_sql.rbs'), 'def instrument: [T] (String name) { () -> T } -> T' + end + assert_includes generate(load_request('pg')).fetch('query_sql.rb'), 'def self.connect(pg_params, logger: nil, **pool_options)' + end end diff --git a/test/golden/mysql2/default/my_queries_sql.rb b/test/golden/mysql2/default/my_queries_sql.rb index 1099c73..05bd12b 100644 --- a/test/golden/mysql2/default/my_queries_sql.rb +++ b/test/golden/mysql2/default/my_queries_sql.rb @@ -13,20 +13,26 @@ module Mysql2Codegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -50,7 +56,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] stmt = client.prepare(WITH_NUMBERS2_GO_SQL) begin @@ -64,10 +70,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/default/my_queries_sql.rbs b/test/golden/mysql2/default/my_queries_sql.rbs index 798ac22..78fae5b 100644 --- a/test/golden/mysql2/default/my_queries_sql.rbs +++ b/test/golden/mysql2/default/my_queries_sql.rbs @@ -3,9 +3,12 @@ module Mysql2Codegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -13,6 +16,8 @@ module Mysql2Codegen private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/mysql2/default/other_sql.rb b/test/golden/mysql2/default/other_sql.rb index 048cedf..f204cec 100644 --- a/test/golden/mysql2/default/other_sql.rb +++ b/test/golden/mysql2/default/other_sql.rb @@ -26,20 +26,26 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -63,7 +69,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -82,7 +88,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| stmt = client.prepare(OTHER_ONE_SQL) begin result = stmt.execute @@ -94,8 +100,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] stmt = client.prepare(COUNT_ROWS_SQL) begin @@ -109,10 +120,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/default/other_sql.rbs b/test/golden/mysql2/default/other_sql.rbs index c84282f..572546b 100644 --- a/test/golden/mysql2/default/other_sql.rbs +++ b/test/golden/mysql2/default/other_sql.rbs @@ -12,9 +12,12 @@ module Mysql2Codegen COUNT_ROWS_SQL: String class OtherSql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -22,10 +25,14 @@ module Mysql2Codegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: Integer) -> Integer private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/mysql2/default/query_sql.rb b/test/golden/mysql2/default/query_sql.rb index e96dcd6..a4cfd49 100644 --- a/test/golden/mysql2/default/query_sql.rb +++ b/test/golden/mysql2/default/query_sql.rb @@ -219,20 +219,26 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -257,7 +263,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] stmt = client.prepare(GET_AUTHOR_SQL) begin @@ -270,8 +276,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| stmt = client.prepare(LIST_AUTHORS_SQL) begin result = stmt.execute @@ -285,7 +296,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_SQL) begin @@ -298,7 +309,7 @@ def create_author(name:, bio:) end def update_author(bio:, id:) - with_client do |client| + with_client('update_author') do |client| query_params = [bio, id] stmt = client.prepare(UPDATE_AUTHOR_SQL) begin @@ -311,7 +322,7 @@ def update_author(bio:, id:) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) begin @@ -324,7 +335,7 @@ def create_author_return_id(name:, bio:) end def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] stmt = client.prepare(DELETE_AUTHOR_SQL) begin @@ -337,7 +348,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| stmt = client.prepare(TEST_SQL) begin result = stmt.execute @@ -387,8 +398,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(limit:, offset:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [limit, offset] stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) begin @@ -403,7 +419,7 @@ def list_authors_paged(limit:, offset:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) begin @@ -419,7 +435,7 @@ def get_author_by_name_pattern(name_pattern: nil) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio: nil) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) begin @@ -432,7 +448,7 @@ def update_author_bios(bio: nil) end def get_authors_by_ids(ids:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [*ids] sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) stmt = client.prepare(sql) @@ -448,7 +464,7 @@ def get_authors_by_ids(ids:) end def get_authors_by_ids_and_names(ids:, names:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [*ids, *names] sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) @@ -466,7 +482,7 @@ def get_authors_by_ids_and_names(ids:, names:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| return if rows.empty? Tempfile.create(['copy_authors', '.csv']) do |file| @@ -481,7 +497,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| stmt = client.prepare(TRUNCATE_AUTHORS_SQL) begin stmt.execute @@ -493,7 +509,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] stmt = client.prepare(CREATE_BOOK_SQL) begin @@ -506,7 +522,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) begin result = stmt.execute(as: :array) @@ -520,7 +536,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) begin result = stmt.execute(as: :array) @@ -534,7 +550,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) begin @@ -549,7 +565,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:, author_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type, author_type] stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) begin @@ -562,7 +578,7 @@ def create_extended_bio(author_name:, name:, bio_type:, author_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) begin @@ -575,8 +591,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) begin stmt.execute @@ -588,7 +609,7 @@ def truncate_extended_bios end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -607,8 +628,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] stmt = client.prepare(MANY_ROWS_SQL) begin @@ -629,7 +655,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| stmt = client.prepare(FOUR_COLS_SQL) begin result = stmt.execute @@ -644,10 +670,24 @@ def four_cols private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/mysql2/default/query_sql.rbs b/test/golden/mysql2/default/query_sql.rbs index f5a0181..a44461a 100644 --- a/test/golden/mysql2/default/query_sql.rbs +++ b/test/golden/mysql2/default/query_sql.rbs @@ -231,14 +231,19 @@ module Mysql2Codegen end class QuerySql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -251,6 +256,8 @@ module Mysql2Codegen def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -277,17 +284,23 @@ module Mysql2Codegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + def many_params!: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow + def many_rows: (c0: Integer) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def csv_line: (Array[untyped] values) -> String diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb index 1099c73..05bd12b 100644 --- a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb @@ -13,20 +13,26 @@ module Mysql2Codegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -50,7 +56,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] stmt = client.prepare(WITH_NUMBERS2_GO_SQL) begin @@ -64,10 +70,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs index 798ac22..78fae5b 100644 --- a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs @@ -3,9 +3,12 @@ module Mysql2Codegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -13,6 +16,8 @@ module Mysql2Codegen private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rb b/test/golden/mysql2/ruby31_gemfile/other_sql.rb index 1be00ad..8595621 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rb @@ -26,20 +26,26 @@ class OtherOneRow < Struct.new(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -63,7 +69,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -82,7 +88,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| stmt = client.prepare(OTHER_ONE_SQL) begin result = stmt.execute @@ -94,8 +100,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] stmt = client.prepare(COUNT_ROWS_SQL) begin @@ -109,10 +120,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs index b3c8c10..b31697b 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs @@ -12,9 +12,12 @@ module Mysql2Codegen COUNT_ROWS_SQL: String class OtherSql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -22,10 +25,14 @@ module Mysql2Codegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: Integer) -> Integer private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rb b/test/golden/mysql2/ruby31_gemfile/query_sql.rb index b5ce0ae..833d0a6 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rb @@ -219,20 +219,26 @@ class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -257,7 +263,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] stmt = client.prepare(GET_AUTHOR_SQL) begin @@ -270,8 +276,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| stmt = client.prepare(LIST_AUTHORS_SQL) begin result = stmt.execute @@ -285,7 +296,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_SQL) begin @@ -298,7 +309,7 @@ def create_author(name:, bio:) end def update_author(bio:, id:) - with_client do |client| + with_client('update_author') do |client| query_params = [bio, id] stmt = client.prepare(UPDATE_AUTHOR_SQL) begin @@ -311,7 +322,7 @@ def update_author(bio:, id:) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) begin @@ -324,7 +335,7 @@ def create_author_return_id(name:, bio:) end def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] stmt = client.prepare(DELETE_AUTHOR_SQL) begin @@ -337,7 +348,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| stmt = client.prepare(TEST_SQL) begin result = stmt.execute @@ -387,8 +398,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(limit:, offset:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [limit, offset] stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) begin @@ -403,7 +419,7 @@ def list_authors_paged(limit:, offset:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) begin @@ -419,7 +435,7 @@ def get_author_by_name_pattern(name_pattern: nil) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio: nil) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) begin @@ -432,7 +448,7 @@ def update_author_bios(bio: nil) end def get_authors_by_ids(ids:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [*ids] sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) stmt = client.prepare(sql) @@ -448,7 +464,7 @@ def get_authors_by_ids(ids:) end def get_authors_by_ids_and_names(ids:, names:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [*ids, *names] sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) @@ -466,7 +482,7 @@ def get_authors_by_ids_and_names(ids:, names:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| return if rows.empty? Tempfile.create(['copy_authors', '.csv']) do |file| @@ -481,7 +497,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| stmt = client.prepare(TRUNCATE_AUTHORS_SQL) begin stmt.execute @@ -493,7 +509,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] stmt = client.prepare(CREATE_BOOK_SQL) begin @@ -506,7 +522,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) begin result = stmt.execute(as: :array) @@ -520,7 +536,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) begin result = stmt.execute(as: :array) @@ -534,7 +550,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) begin @@ -549,7 +565,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:, author_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type, author_type] stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) begin @@ -562,7 +578,7 @@ def create_extended_bio(author_name:, name:, bio_type:, author_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) begin @@ -575,8 +591,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) begin stmt.execute @@ -588,7 +609,7 @@ def truncate_extended_bios end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -607,8 +628,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] stmt = client.prepare(MANY_ROWS_SQL) begin @@ -629,7 +655,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| stmt = client.prepare(FOUR_COLS_SQL) begin result = stmt.execute @@ -644,10 +670,24 @@ def four_cols private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs index 8bedc4a..848d0a2 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -231,14 +231,19 @@ module Mysql2Codegen end class QuerySql - def self.connect: (Hash[untyped, untyped] mysql2_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] mysql2_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -251,6 +256,8 @@ module Mysql2Codegen def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (limit: Integer, offset: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -277,17 +284,23 @@ module Mysql2Codegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + def many_params!: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow + def many_rows: (c0: Integer) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def csv_line: (Array[untyped] values) -> String diff --git a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb index 1099c73..05bd12b 100644 --- a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb @@ -13,20 +13,26 @@ module Mysql2Codegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -50,7 +56,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] stmt = client.prepare(WITH_NUMBERS2_GO_SQL) begin @@ -64,10 +70,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/ruby32_notypes/other_sql.rb b/test/golden/mysql2/ruby32_notypes/other_sql.rb index 048cedf..f204cec 100644 --- a/test/golden/mysql2/ruby32_notypes/other_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/other_sql.rb @@ -26,20 +26,26 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -63,7 +69,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -82,7 +88,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| stmt = client.prepare(OTHER_ONE_SQL) begin result = stmt.execute @@ -94,8 +100,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] stmt = client.prepare(COUNT_ROWS_SQL) begin @@ -109,10 +120,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end end diff --git a/test/golden/mysql2/ruby32_notypes/query_sql.rb b/test/golden/mysql2/ruby32_notypes/query_sql.rb index e96dcd6..a4cfd49 100644 --- a/test/golden/mysql2/ruby32_notypes/query_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/query_sql.rb @@ -219,20 +219,26 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ database: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(mysql2_params, **pool_options) - new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }) + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a Mysql2::Client). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = !client.in_transaction savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -257,7 +263,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] stmt = client.prepare(GET_AUTHOR_SQL) begin @@ -270,8 +276,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| stmt = client.prepare(LIST_AUTHORS_SQL) begin result = stmt.execute @@ -285,7 +296,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_SQL) begin @@ -298,7 +309,7 @@ def create_author(name:, bio:) end def update_author(bio:, id:) - with_client do |client| + with_client('update_author') do |client| query_params = [bio, id] stmt = client.prepare(UPDATE_AUTHOR_SQL) begin @@ -311,7 +322,7 @@ def update_author(bio:, id:) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) begin @@ -324,7 +335,7 @@ def create_author_return_id(name:, bio:) end def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] stmt = client.prepare(DELETE_AUTHOR_SQL) begin @@ -337,7 +348,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| stmt = client.prepare(TEST_SQL) begin result = stmt.execute @@ -387,8 +398,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(limit:, offset:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [limit, offset] stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) begin @@ -403,7 +419,7 @@ def list_authors_paged(limit:, offset:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) begin @@ -419,7 +435,7 @@ def get_author_by_name_pattern(name_pattern: nil) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio: nil) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) begin @@ -432,7 +448,7 @@ def update_author_bios(bio: nil) end def get_authors_by_ids(ids:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [*ids] sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) stmt = client.prepare(sql) @@ -448,7 +464,7 @@ def get_authors_by_ids(ids:) end def get_authors_by_ids_and_names(ids:, names:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [*ids, *names] sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) @@ -466,7 +482,7 @@ def get_authors_by_ids_and_names(ids:, names:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| return if rows.empty? Tempfile.create(['copy_authors', '.csv']) do |file| @@ -481,7 +497,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| stmt = client.prepare(TRUNCATE_AUTHORS_SQL) begin stmt.execute @@ -493,7 +509,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] stmt = client.prepare(CREATE_BOOK_SQL) begin @@ -506,7 +522,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) begin result = stmt.execute(as: :array) @@ -520,7 +536,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) begin result = stmt.execute(as: :array) @@ -534,7 +550,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) begin @@ -549,7 +565,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:, author_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type, author_type] stmt = client.prepare(CREATE_EXTENDED_BIO_SQL) begin @@ -562,7 +578,7 @@ def create_extended_bio(author_name:, name:, bio_type:, author_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] stmt = client.prepare(GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) begin @@ -575,8 +591,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) begin stmt.execute @@ -588,7 +609,7 @@ def truncate_extended_bios end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -607,8 +628,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] stmt = client.prepare(MANY_ROWS_SQL) begin @@ -629,7 +655,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| stmt = client.prepare(FOUR_COLS_SQL) begin result = stmt.execute @@ -644,10 +670,24 @@ def four_cols private - def with_client - @db.with do |client| - client.extend(ConnectionState) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/default/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb index b56f64e..813f87e 100644 --- a/test/golden/pg/default/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -13,20 +13,26 @@ module PgCodegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -46,7 +52,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) @@ -59,10 +65,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/default/my_queries_sql.rbs b/test/golden/pg/default/my_queries_sql.rbs index 8a4818c..c31b4fb 100644 --- a/test/golden/pg/default/my_queries_sql.rbs +++ b/test/golden/pg/default/my_queries_sql.rbs @@ -3,9 +3,12 @@ module PgCodegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -13,7 +16,9 @@ module PgCodegen private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/default/other_sql.rb b/test/golden/pg/default/other_sql.rb index 1a6fb1f..91b8418 100644 --- a/test/golden/pg/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -26,20 +26,26 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -59,7 +65,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -77,7 +83,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| unless client.prepared_statements.include?('other_sql_other_one') client.prepare('other_sql_other_one', OTHER_ONE_SQL) client.prepared_statements.add('other_sql_other_one') @@ -88,8 +94,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('other_sql_count_rows') client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) @@ -102,10 +113,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/default/other_sql.rbs b/test/golden/pg/default/other_sql.rbs index ae3ba27..e3a1a3f 100644 --- a/test/golden/pg/default/other_sql.rbs +++ b/test/golden/pg/default/other_sql.rbs @@ -12,9 +12,12 @@ module PgCodegen COUNT_ROWS_SQL: String class OtherSql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -22,11 +25,15 @@ module PgCodegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: bool) -> Integer private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb index aa15016..3221290 100644 --- a/test/golden/pg/default/query_sql.rb +++ b/test/golden/pg/default/query_sql.rb @@ -234,20 +234,26 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -268,7 +274,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_get_author') client.prepare('query_sql_get_author', GET_AUTHOR_SQL) @@ -280,8 +286,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| unless client.prepared_statements.include?('query_sql_list_authors') client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) client.prepared_statements.add('query_sql_list_authors') @@ -294,7 +305,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author') client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) @@ -306,8 +317,13 @@ def create_author(name:, bio:) end end + # Like #create_author, but raises NotFound when there is no row. + def create_author!(...) + create_author(...) || raise(NotFound, 'CreateAuthor returned no row') + end + def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_delete_author') client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) @@ -319,7 +335,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| unless client.prepared_statements.include?('query_sql_test') client.prepare('query_sql_test', TEST_SQL) client.prepared_statements.add('query_sql_test') @@ -349,8 +365,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(offset:, limit:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [offset, limit] unless client.prepared_statements.include?('query_sql_list_authors_paged') client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) @@ -364,7 +385,7 @@ def list_authors_paged(offset:, limit:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) @@ -378,7 +399,7 @@ def get_author_by_name_pattern(name_pattern: nil) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author_return_id') client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) @@ -391,7 +412,7 @@ def create_author_return_id(name:, bio:) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] unless client.prepared_statements.include?('query_sql_update_author_bios') client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) @@ -403,7 +424,7 @@ def update_author_bios(bio:) end def get_authors_by_ids(param1:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [param1] unless client.prepared_statements.include?('query_sql_get_authors_by_ids') client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) @@ -417,7 +438,7 @@ def get_authors_by_ids(param1:) end def get_authors_by_ids_and_names(param1:, param2:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [param1, param2] unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) @@ -432,7 +453,7 @@ def get_authors_by_ids_and_names(param1:, param2:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do rows.each do |row| @@ -444,7 +465,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| unless client.prepared_statements.include?('query_sql_truncate_authors') client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_truncate_authors') @@ -455,7 +476,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] unless client.prepared_statements.include?('query_sql_create_book') client.prepare('query_sql_create_book', CREATE_BOOK_SQL) @@ -467,7 +488,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| unless client.prepared_statements.include?('query_sql_list_all_authors_books') client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) client.prepared_statements.add('query_sql_list_all_authors_books') @@ -480,7 +501,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| unless client.prepared_statements.include?('query_sql_get_duplicate_authors') client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_get_duplicate_authors') @@ -493,7 +514,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) @@ -507,7 +528,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type] unless client.prepared_statements.include?('query_sql_create_extended_bio') client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) @@ -519,7 +540,7 @@ def create_extended_bio(author_name:, name:, bio_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) @@ -531,8 +552,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| unless client.prepared_statements.include?('query_sql_truncate_extended_bios') client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) client.prepared_statements.add('query_sql_truncate_extended_bios') @@ -543,7 +569,7 @@ def truncate_extended_bios end def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) - with_client do |client| + with_client('insert_extra_types') do |client| query_params = [ c_uuid, c_jsonb, @@ -564,7 +590,7 @@ def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_tim end def get_extra_types - with_client do |client| + with_client('get_extra_types') do |client| unless client.prepared_statements.include?('query_sql_get_extra_types') client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_get_extra_types') @@ -584,8 +610,13 @@ def get_extra_types end end + # Like #get_extra_types, but raises NotFound when there is no row. + def get_extra_types!(...) + get_extra_types(...) || raise(NotFound, 'GetExtraTypes returned no row') + end + def truncate_extra_types - with_client do |client| + with_client('truncate_extra_types') do |client| unless client.prepared_statements.include?('query_sql_truncate_extra_types') client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_truncate_extra_types') @@ -596,7 +627,7 @@ def truncate_extra_types end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -614,8 +645,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('query_sql_many_rows') client.prepare('query_sql_many_rows', MANY_ROWS_SQL) @@ -635,7 +671,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| unless client.prepared_statements.include?('query_sql_four_cols') client.prepare('query_sql_four_cols', FOUR_COLS_SQL) client.prepared_statements.add('query_sql_four_cols') @@ -649,10 +685,24 @@ def four_cols private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs index fdc8fbf..28acc6c 100644 --- a/test/golden/pg/default/query_sql.rbs +++ b/test/golden/pg/default/query_sql.rbs @@ -234,22 +234,31 @@ module PgCodegen end class QuerySql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> CreateAuthorRow? + def create_author!: (name: String, bio: String?) -> CreateAuthorRow + def delete_author: (id: Integer) -> void def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -278,23 +287,31 @@ module PgCodegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void def get_extra_types: -> GetExtraTypesRow? + def get_extra_types!: -> GetExtraTypesRow + def truncate_extra_types: -> void def many_params: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow? + def many_params!: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow + def many_rows: (c0: bool) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb index 5464449..1982f7c 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -14,20 +14,26 @@ module PgCodegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -47,7 +53,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) @@ -60,10 +66,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs index 8a4818c..c31b4fb 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs @@ -3,9 +3,12 @@ module PgCodegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -13,7 +16,9 @@ module PgCodegen private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb index c92eb76..fc9b664 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -27,20 +27,26 @@ class OtherOneRow < Struct.new(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -60,7 +66,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -78,7 +84,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| unless client.prepared_statements.include?('other_sql_other_one') client.prepare('other_sql_other_one', OTHER_ONE_SQL) client.prepared_statements.add('other_sql_other_one') @@ -89,8 +95,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('other_sql_count_rows') client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) @@ -103,10 +114,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rbs b/test/golden/pg/ruby31_gemfile/other_sql.rbs index 582ff48..e422645 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -12,9 +12,12 @@ module PgCodegen COUNT_ROWS_SQL: String class OtherSql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T @@ -22,11 +25,15 @@ module PgCodegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: bool) -> Integer private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb index 565cbc2..1f47341 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rb +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -235,20 +235,26 @@ class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -269,7 +275,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_get_author') client.prepare('query_sql_get_author', GET_AUTHOR_SQL) @@ -281,8 +287,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| unless client.prepared_statements.include?('query_sql_list_authors') client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) client.prepared_statements.add('query_sql_list_authors') @@ -295,7 +306,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author') client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) @@ -307,8 +318,13 @@ def create_author(name:, bio:) end end + # Like #create_author, but raises NotFound when there is no row. + def create_author!(...) + create_author(...) || raise(NotFound, 'CreateAuthor returned no row') + end + def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_delete_author') client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) @@ -320,7 +336,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| unless client.prepared_statements.include?('query_sql_test') client.prepare('query_sql_test', TEST_SQL) client.prepared_statements.add('query_sql_test') @@ -350,8 +366,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(offset:, limit:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [offset, limit] unless client.prepared_statements.include?('query_sql_list_authors_paged') client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) @@ -365,7 +386,7 @@ def list_authors_paged(offset:, limit:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) @@ -379,7 +400,7 @@ def get_author_by_name_pattern(name_pattern: nil) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author_return_id') client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) @@ -392,7 +413,7 @@ def create_author_return_id(name:, bio:) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] unless client.prepared_statements.include?('query_sql_update_author_bios') client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) @@ -404,7 +425,7 @@ def update_author_bios(bio:) end def get_authors_by_ids(param1:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [param1] unless client.prepared_statements.include?('query_sql_get_authors_by_ids') client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) @@ -418,7 +439,7 @@ def get_authors_by_ids(param1:) end def get_authors_by_ids_and_names(param1:, param2:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [param1, param2] unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) @@ -433,7 +454,7 @@ def get_authors_by_ids_and_names(param1:, param2:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do rows.each do |row| @@ -445,7 +466,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| unless client.prepared_statements.include?('query_sql_truncate_authors') client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_truncate_authors') @@ -456,7 +477,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] unless client.prepared_statements.include?('query_sql_create_book') client.prepare('query_sql_create_book', CREATE_BOOK_SQL) @@ -468,7 +489,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| unless client.prepared_statements.include?('query_sql_list_all_authors_books') client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) client.prepared_statements.add('query_sql_list_all_authors_books') @@ -481,7 +502,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| unless client.prepared_statements.include?('query_sql_get_duplicate_authors') client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_get_duplicate_authors') @@ -494,7 +515,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) @@ -508,7 +529,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type] unless client.prepared_statements.include?('query_sql_create_extended_bio') client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) @@ -520,7 +541,7 @@ def create_extended_bio(author_name:, name:, bio_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) @@ -532,8 +553,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| unless client.prepared_statements.include?('query_sql_truncate_extended_bios') client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) client.prepared_statements.add('query_sql_truncate_extended_bios') @@ -544,7 +570,7 @@ def truncate_extended_bios end def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) - with_client do |client| + with_client('insert_extra_types') do |client| query_params = [ c_uuid, c_jsonb, @@ -565,7 +591,7 @@ def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_tim end def get_extra_types - with_client do |client| + with_client('get_extra_types') do |client| unless client.prepared_statements.include?('query_sql_get_extra_types') client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_get_extra_types') @@ -585,8 +611,13 @@ def get_extra_types end end + # Like #get_extra_types, but raises NotFound when there is no row. + def get_extra_types!(...) + get_extra_types(...) || raise(NotFound, 'GetExtraTypes returned no row') + end + def truncate_extra_types - with_client do |client| + with_client('truncate_extra_types') do |client| unless client.prepared_statements.include?('query_sql_truncate_extra_types') client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_truncate_extra_types') @@ -597,7 +628,7 @@ def truncate_extra_types end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -615,8 +646,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('query_sql_many_rows') client.prepare('query_sql_many_rows', MANY_ROWS_SQL) @@ -636,7 +672,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| unless client.prepared_statements.include?('query_sql_four_cols') client.prepare('query_sql_four_cols', FOUR_COLS_SQL) client.prepared_statements.add('query_sql_four_cols') @@ -650,10 +686,24 @@ def four_cols private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs index ecd1121..e849f4e 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -234,22 +234,31 @@ module PgCodegen end class QuerySql - def self.connect: (Hash[untyped, untyped] pg_params, **untyped pool_options) -> instance + class NotFound < StandardError + end - def initialize: (untyped db) -> void + def self.connect: (Hash[untyped, untyped] pg_params, ?logger: untyped, **untyped pool_options) -> instance + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> CreateAuthorRow? + def create_author!: (name: String, bio: String?) -> CreateAuthorRow + def delete_author: (id: Integer) -> void def test: -> TestRow? + def test!: -> TestRow + def list_authors_paged: (offset: Integer, limit: Integer) -> Array[ListAuthorsPagedRow] def get_author_by_name_pattern: (?name_pattern: String?) -> Array[GetAuthorByNamePatternRow] @@ -278,23 +287,31 @@ module PgCodegen def get_first_extended_bio_by_type: (bio_type: String?) -> GetFirstExtendedBioByTypeRow? + def get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + def truncate_extended_bios: -> void def insert_extra_types: (c_uuid: String?, c_jsonb: untyped, c_text_array: Array[String]?, c_integer_array: Array[Integer]?, c_timestamptz: Time?, c_inet: IPAddr?, c_money: String?, c_not_null: String) -> void def get_extra_types: -> GetExtraTypesRow? + def get_extra_types!: -> GetExtraTypesRow + def truncate_extra_types: -> void def many_params: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow? + def many_params!: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> ManyParamsRow + def many_rows: (c0: bool) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private - def with_client: [T] () { (untyped) -> T } -> T + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T def configure: (untyped client) -> void end diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb index b56f64e..813f87e 100644 --- a/test/golden/pg/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -13,20 +13,26 @@ module PgCodegen SQL class My_queriesSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -46,7 +52,7 @@ def transaction(isolation: nil) end def with_numbers2_go(c0:) - with_client do |client| + with_client('with_numbers2_go') do |client| query_params = [c0] unless client.prepared_statements.include?('my_queries_sql_with_numbers2_go') client.prepare('my_queries_sql_with_numbers2_go', WITH_NUMBERS2_GO_SQL) @@ -59,10 +65,24 @@ def with_numbers2_go(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb index 1a6fb1f..91b8418 100644 --- a/test/golden/pg/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -26,20 +26,26 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -59,7 +65,7 @@ def transaction(isolation: nil) end def no_cols(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('no_cols') do |client| query_params = [ c0, c1, @@ -77,7 +83,7 @@ def no_cols(c0:, c1:, c2:, c3:, c4:) end def other_one - with_client do |client| + with_client('other_one') do |client| unless client.prepared_statements.include?('other_sql_other_one') client.prepare('other_sql_other_one', OTHER_ONE_SQL) client.prepared_statements.add('other_sql_other_one') @@ -88,8 +94,13 @@ def other_one end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - with_client do |client| + with_client('count_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('other_sql_count_rows') client.prepare('other_sql_count_rows', COUNT_ROWS_SQL) @@ -102,10 +113,24 @@ def count_rows(c0:) private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb index aa15016..3221290 100644 --- a/test/golden/pg/ruby32_notypes/query_sql.rb +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -234,20 +234,26 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # Opens a pool of connections: QuerySql.connect({ dbname: 'app' }, size: 5, timeout: 5, logger: Logger.new($stdout)) # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). - def self.connect(pg_params, **pool_options) - new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }) + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) end - def initialize(db) + # db: a ConnectionPool (or anything that responds to #with and yields a PG::Connection). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction on one connection and returns its value: COMMIT when the block returns, # ROLLBACK when it raises. A nested call becomes a SAVEPOINT. Yields self, bound to that connection. def transaction(isolation: nil) - with_client do |client| + with_client('transaction') do |client| outermost = client.transaction_status == PG::PQTRANS_IDLE savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" if outermost @@ -268,7 +274,7 @@ def transaction(isolation: nil) # Returns the author with the given id, or nil when there is none. def get_author(id:) - with_client do |client| + with_client('get_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_get_author') client.prepare('query_sql_get_author', GET_AUTHOR_SQL) @@ -280,8 +286,13 @@ def get_author(id:) end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - with_client do |client| + with_client('list_authors') do |client| unless client.prepared_statements.include?('query_sql_list_authors') client.prepare('query_sql_list_authors', LIST_AUTHORS_SQL) client.prepared_statements.add('query_sql_list_authors') @@ -294,7 +305,7 @@ def list_authors end def create_author(name:, bio:) - with_client do |client| + with_client('create_author') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author') client.prepare('query_sql_create_author', CREATE_AUTHOR_SQL) @@ -306,8 +317,13 @@ def create_author(name:, bio:) end end + # Like #create_author, but raises NotFound when there is no row. + def create_author!(...) + create_author(...) || raise(NotFound, 'CreateAuthor returned no row') + end + def delete_author(id:) - with_client do |client| + with_client('delete_author') do |client| query_params = [id] unless client.prepared_statements.include?('query_sql_delete_author') client.prepare('query_sql_delete_author', DELETE_AUTHOR_SQL) @@ -319,7 +335,7 @@ def delete_author(id:) end def test - with_client do |client| + with_client('test') do |client| unless client.prepared_statements.include?('query_sql_test') client.prepare('query_sql_test', TEST_SQL) client.prepared_statements.add('query_sql_test') @@ -349,8 +365,13 @@ def test end end + # Like #test, but raises NotFound when there is no row. + def test!(...) + test(...) || raise(NotFound, 'Test returned no row') + end + def list_authors_paged(offset:, limit:) - with_client do |client| + with_client('list_authors_paged') do |client| query_params = [offset, limit] unless client.prepared_statements.include?('query_sql_list_authors_paged') client.prepare('query_sql_list_authors_paged', LIST_AUTHORS_PAGED_SQL) @@ -364,7 +385,7 @@ def list_authors_paged(offset:, limit:) end def get_author_by_name_pattern(name_pattern: nil) - with_client do |client| + with_client('get_author_by_name_pattern') do |client| query_params = [name_pattern] unless client.prepared_statements.include?('query_sql_get_author_by_name_pattern') client.prepare('query_sql_get_author_by_name_pattern', GET_AUTHOR_BY_NAME_PATTERN_SQL) @@ -378,7 +399,7 @@ def get_author_by_name_pattern(name_pattern: nil) end def create_author_return_id(name:, bio:) - with_client do |client| + with_client('create_author_return_id') do |client| query_params = [name, bio] unless client.prepared_statements.include?('query_sql_create_author_return_id') client.prepare('query_sql_create_author_return_id', CREATE_AUTHOR_RETURN_ID_SQL) @@ -391,7 +412,7 @@ def create_author_return_id(name:, bio:) # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - with_client do |client| + with_client('update_author_bios') do |client| query_params = [bio] unless client.prepared_statements.include?('query_sql_update_author_bios') client.prepare('query_sql_update_author_bios', UPDATE_AUTHOR_BIOS_SQL) @@ -403,7 +424,7 @@ def update_author_bios(bio:) end def get_authors_by_ids(param1:) - with_client do |client| + with_client('get_authors_by_ids') do |client| query_params = [param1] unless client.prepared_statements.include?('query_sql_get_authors_by_ids') client.prepare('query_sql_get_authors_by_ids', GET_AUTHORS_BY_IDS_SQL) @@ -417,7 +438,7 @@ def get_authors_by_ids(param1:) end def get_authors_by_ids_and_names(param1:, param2:) - with_client do |client| + with_client('get_authors_by_ids_and_names') do |client| query_params = [param1, param2] unless client.prepared_statements.include?('query_sql_get_authors_by_ids_and_names') client.prepare('query_sql_get_authors_by_ids_and_names', GET_AUTHORS_BY_IDS_AND_NAMES_SQL) @@ -432,7 +453,7 @@ def get_authors_by_ids_and_names(param1:, param2:) # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - with_client do |client| + with_client('copy_authors') do |client| encoder = PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) client.copy_data('COPY authors (name, bio) FROM STDIN', encoder) do rows.each do |row| @@ -444,7 +465,7 @@ def copy_authors(rows) end def truncate_authors - with_client do |client| + with_client('truncate_authors') do |client| unless client.prepared_statements.include?('query_sql_truncate_authors') client.prepare('query_sql_truncate_authors', TRUNCATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_truncate_authors') @@ -455,7 +476,7 @@ def truncate_authors end def create_book(name:, author_id:) - with_client do |client| + with_client('create_book') do |client| query_params = [name, author_id] unless client.prepared_statements.include?('query_sql_create_book') client.prepare('query_sql_create_book', CREATE_BOOK_SQL) @@ -467,7 +488,7 @@ def create_book(name:, author_id:) end def list_all_authors_books - with_client do |client| + with_client('list_all_authors_books') do |client| unless client.prepared_statements.include?('query_sql_list_all_authors_books') client.prepare('query_sql_list_all_authors_books', LIST_ALL_AUTHORS_BOOKS_SQL) client.prepared_statements.add('query_sql_list_all_authors_books') @@ -480,7 +501,7 @@ def list_all_authors_books end def get_duplicate_authors - with_client do |client| + with_client('get_duplicate_authors') do |client| unless client.prepared_statements.include?('query_sql_get_duplicate_authors') client.prepare('query_sql_get_duplicate_authors', GET_DUPLICATE_AUTHORS_SQL) client.prepared_statements.add('query_sql_get_duplicate_authors') @@ -493,7 +514,7 @@ def get_duplicate_authors end def get_authors_by_book_name(name:) - with_client do |client| + with_client('get_authors_by_book_name') do |client| query_params = [name] unless client.prepared_statements.include?('query_sql_get_authors_by_book_name') client.prepare('query_sql_get_authors_by_book_name', GET_AUTHORS_BY_BOOK_NAME_SQL) @@ -507,7 +528,7 @@ def get_authors_by_book_name(name:) end def create_extended_bio(author_name:, name:, bio_type:) - with_client do |client| + with_client('create_extended_bio') do |client| query_params = [author_name, name, bio_type] unless client.prepared_statements.include?('query_sql_create_extended_bio') client.prepare('query_sql_create_extended_bio', CREATE_EXTENDED_BIO_SQL) @@ -519,7 +540,7 @@ def create_extended_bio(author_name:, name:, bio_type:) end def get_first_extended_bio_by_type(bio_type:) - with_client do |client| + with_client('get_first_extended_bio_by_type') do |client| query_params = [bio_type] unless client.prepared_statements.include?('query_sql_get_first_extended_bio_by_type') client.prepare('query_sql_get_first_extended_bio_by_type', GET_FIRST_EXTENDED_BIO_BY_TYPE_SQL) @@ -531,8 +552,13 @@ def get_first_extended_bio_by_type(bio_type:) end end + # Like #get_first_extended_bio_by_type, but raises NotFound when there is no row. + def get_first_extended_bio_by_type!(...) + get_first_extended_bio_by_type(...) || raise(NotFound, 'GetFirstExtendedBioByType returned no row') + end + def truncate_extended_bios - with_client do |client| + with_client('truncate_extended_bios') do |client| unless client.prepared_statements.include?('query_sql_truncate_extended_bios') client.prepare('query_sql_truncate_extended_bios', TRUNCATE_EXTENDED_BIOS_SQL) client.prepared_statements.add('query_sql_truncate_extended_bios') @@ -543,7 +569,7 @@ def truncate_extended_bios end def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_timestamptz:, c_inet:, c_money:, c_not_null:) - with_client do |client| + with_client('insert_extra_types') do |client| query_params = [ c_uuid, c_jsonb, @@ -564,7 +590,7 @@ def insert_extra_types(c_uuid:, c_jsonb:, c_text_array:, c_integer_array:, c_tim end def get_extra_types - with_client do |client| + with_client('get_extra_types') do |client| unless client.prepared_statements.include?('query_sql_get_extra_types') client.prepare('query_sql_get_extra_types', GET_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_get_extra_types') @@ -584,8 +610,13 @@ def get_extra_types end end + # Like #get_extra_types, but raises NotFound when there is no row. + def get_extra_types!(...) + get_extra_types(...) || raise(NotFound, 'GetExtraTypes returned no row') + end + def truncate_extra_types - with_client do |client| + with_client('truncate_extra_types') do |client| unless client.prepared_statements.include?('query_sql_truncate_extra_types') client.prepare('query_sql_truncate_extra_types', TRUNCATE_EXTRA_TYPES_SQL) client.prepared_statements.add('query_sql_truncate_extra_types') @@ -596,7 +627,7 @@ def truncate_extra_types end def many_params(c0:, c1:, c2:, c3:, c4:) - with_client do |client| + with_client('many_params') do |client| query_params = [ c0, c1, @@ -614,8 +645,13 @@ def many_params(c0:, c1:, c2:, c3:, c4:) end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - with_client do |client| + with_client('many_rows') do |client| query_params = [c0] unless client.prepared_statements.include?('query_sql_many_rows') client.prepare('query_sql_many_rows', MANY_ROWS_SQL) @@ -635,7 +671,7 @@ def many_rows(c0:) end def four_cols - with_client do |client| + with_client('four_cols') do |client| unless client.prepared_statements.include?('query_sql_four_cols') client.prepare('query_sql_four_cols', FOUR_COLS_SQL) client.prepared_statements.add('query_sql_four_cols') @@ -649,10 +685,24 @@ def four_cols private - def with_client - @db.with do |client| - configure(client) unless client.is_a?(ConnectionState) - yield client + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end end end diff --git a/test/golden/sqlite3/default/my_queries_sql.rb b/test/golden/sqlite3/default/my_queries_sql.rb index 6ad5713..11eaa39 100644 --- a/test/golden/sqlite3/default/my_queries_sql.rb +++ b/test/golden/sqlite3/default/my_queries_sql.rb @@ -12,37 +12,63 @@ module Sqlite3Codegen SQL class My_queriesSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def with_numbers2_go(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + with_client('with_numbers2_go') do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - nil + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/default/my_queries_sql.rbs b/test/golden/sqlite3/default/my_queries_sql.rbs index 52b5923..290d346 100644 --- a/test/golden/sqlite3/default/my_queries_sql.rbs +++ b/test/golden/sqlite3/default/my_queries_sql.rbs @@ -3,10 +3,19 @@ module Sqlite3Codegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T def with_numbers2_go: (c0: Integer) -> void + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/sqlite3/default/other_sql.rb b/test/golden/sqlite3/default/other_sql.rb index 48bd104..677ee1b 100644 --- a/test/golden/sqlite3/default/other_sql.rb +++ b/test/golden/sqlite3/default/other_sql.rb @@ -25,67 +25,100 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def no_cols(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(NO_COLS_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('no_cols') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def other_one - client = @db - stmt = client.prepare(OTHER_ONE_SQL) - begin - result = stmt.execute - row = result.first - row && OtherOneRow.new(row[0]) - ensure - stmt.close + with_client('other_one') do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(COUNT_ROWS_SQL) + with_client('count_rows') do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - client.changes + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/default/other_sql.rbs b/test/golden/sqlite3/default/other_sql.rbs index 2eac5c8..6d993a0 100644 --- a/test/golden/sqlite3/default/other_sql.rbs +++ b/test/golden/sqlite3/default/other_sql.rbs @@ -12,7 +12,10 @@ module Sqlite3Codegen COUNT_ROWS_SQL: String class OtherSql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T @@ -20,6 +23,14 @@ module Sqlite3Codegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: Integer) -> Integer + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/sqlite3/default/query_sql.rb b/test/golden/sqlite3/default/query_sql.rb index 8cfab6a..a6c17be 100644 --- a/test/golden/sqlite3/default/query_sql.rb +++ b/test/golden/sqlite3/default/query_sql.rb @@ -158,291 +158,343 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end # Returns the author with the given id, or nil when there is none. def get_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(GET_AUTHOR_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && GetAuthorRow.new(row[0], row[1], row[2]) - ensure - stmt.close + with_client('get_author') do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - client = @db - stmt = client.prepare(LIST_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAuthorsRow.new(row[0], row[1], row[2]) + with_client('list_authors') do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('create_author') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def delete_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(DELETE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('delete_author') do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def list_authors_paged(offset:, limit:) - client = @db - query_params = [offset, limit] - stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - ListAuthorsPagedRow.new(row[0], row[1], row[2]) + with_client('list_authors_paged') do |client| + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_author_by_name_pattern(name_pattern: nil) - client = @db - query_params = [name_pattern] - stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + with_client('get_author_by_name_pattern') do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author_return_id(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_author_return_id') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - client = @db - query_params = [bio] - stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) - begin - stmt.execute(*query_params) - client.changes - ensure - stmt.close + with_client('update_author_bios') do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end end end def get_authors_by_ids(ids:) - client = @db - query_params = [*ids] - sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids') do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_ids_and_names(ids:, names:) - client = @db - query_params = [*ids, *names] - sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids_and_names') do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - client = @db - rows.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + with_client('copy_authors') do |client| + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil end - nil end def truncate_authors - client = @db - stmt = client.prepare(TRUNCATE_AUTHORS_SQL) - begin - stmt.execute - nil - ensure - stmt.close + with_client('truncate_authors') do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end end end def create_book(name:, author_id:) - client = @db - query_params = [name, author_id] - stmt = client.prepare(CREATE_BOOK_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_book') do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end def list_all_authors_books - client = @db - stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + with_client('list_all_authors_books') do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_duplicate_authors - client = @db - stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + with_client('get_duplicate_authors') do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_book_name(name:) - client = @db - query_params = [name] - stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + with_client('get_authors_by_book_name') do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def many_params(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(MANY_PARAMS_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && ManyParamsRow.new(row[0], row[1]) - ensure - stmt.close + with_client('many_params') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(MANY_ROWS_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) + with_client('many_rows') do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close end - ensure - stmt.close end end def four_cols - client = @db - stmt = client.prepare(FOUR_COLS_SQL) - begin - result = stmt.execute - result.map do |row| - FourColsRow.new(row[0], row[1], row[2], row[3]) + with_client('four_cols') do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close end - ensure - stmt.close end end private + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end + def slice_placeholders(values) values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') end diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs index d7de3f0..d4bd9f5 100644 --- a/test/golden/sqlite3/default/query_sql.rbs +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -144,12 +144,17 @@ module Sqlite3Codegen end class QuerySql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -182,12 +187,18 @@ module Sqlite3Codegen def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + def many_params!: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow + def many_rows: (c0: Integer) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T + def slice_placeholders: (Array[untyped] values) -> String end end diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb index 6ad5713..11eaa39 100644 --- a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb @@ -12,37 +12,63 @@ module Sqlite3Codegen SQL class My_queriesSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def with_numbers2_go(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + with_client('with_numbers2_go') do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - nil + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs index 52b5923..290d346 100644 --- a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs @@ -3,10 +3,19 @@ module Sqlite3Codegen WITH_NUMBERS2_GO_SQL: String class My_queriesSql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T def with_numbers2_go: (c0: Integer) -> void + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rb b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb index ba3ce95..dc840cd 100644 --- a/test/golden/sqlite3/ruby31_gemfile/other_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb @@ -25,67 +25,100 @@ class OtherOneRow < Struct.new(:c0) SQL class OtherSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def no_cols(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(NO_COLS_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('no_cols') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def other_one - client = @db - stmt = client.prepare(OTHER_ONE_SQL) - begin - result = stmt.execute - row = result.first - row && OtherOneRow.new(row[0]) - ensure - stmt.close + with_client('other_one') do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(COUNT_ROWS_SQL) + with_client('count_rows') do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - client.changes + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs index 271a9aa..2791fe8 100644 --- a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs @@ -12,7 +12,10 @@ module Sqlite3Codegen COUNT_ROWS_SQL: String class OtherSql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T @@ -20,6 +23,14 @@ module Sqlite3Codegen def other_one: -> OtherOneRow? + def other_one!: -> OtherOneRow + def count_rows: (c0: Integer) -> Integer + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T end end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb index 627d509..33e66f8 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -158,291 +158,343 @@ class FourColsRow < Struct.new(:c0, :c1, :c2, :c3) end class QuerySql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end # Returns the author with the given id, or nil when there is none. def get_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(GET_AUTHOR_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && GetAuthorRow.new(row[0], row[1], row[2]) - ensure - stmt.close + with_client('get_author') do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - client = @db - stmt = client.prepare(LIST_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAuthorsRow.new(row[0], row[1], row[2]) + with_client('list_authors') do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('create_author') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def delete_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(DELETE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('delete_author') do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def list_authors_paged(offset:, limit:) - client = @db - query_params = [offset, limit] - stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - ListAuthorsPagedRow.new(row[0], row[1], row[2]) + with_client('list_authors_paged') do |client| + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_author_by_name_pattern(name_pattern: nil) - client = @db - query_params = [name_pattern] - stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + with_client('get_author_by_name_pattern') do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author_return_id(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_author_return_id') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - client = @db - query_params = [bio] - stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) - begin - stmt.execute(*query_params) - client.changes - ensure - stmt.close + with_client('update_author_bios') do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end end end def get_authors_by_ids(ids:) - client = @db - query_params = [*ids] - sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids') do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_ids_and_names(ids:, names:) - client = @db - query_params = [*ids, *names] - sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids_and_names') do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - client = @db - rows.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + with_client('copy_authors') do |client| + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil end - nil end def truncate_authors - client = @db - stmt = client.prepare(TRUNCATE_AUTHORS_SQL) - begin - stmt.execute - nil - ensure - stmt.close + with_client('truncate_authors') do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end end end def create_book(name:, author_id:) - client = @db - query_params = [name, author_id] - stmt = client.prepare(CREATE_BOOK_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_book') do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end def list_all_authors_books - client = @db - stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + with_client('list_all_authors_books') do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_duplicate_authors - client = @db - stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + with_client('get_duplicate_authors') do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_book_name(name:) - client = @db - query_params = [name] - stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + with_client('get_authors_by_book_name') do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def many_params(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(MANY_PARAMS_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && ManyParamsRow.new(row[0], row[1]) - ensure - stmt.close + with_client('many_params') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(MANY_ROWS_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) + with_client('many_rows') do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close end - ensure - stmt.close end end def four_cols - client = @db - stmt = client.prepare(FOUR_COLS_SQL) - begin - result = stmt.execute - result.map do |row| - FourColsRow.new(row[0], row[1], row[2], row[3]) + with_client('four_cols') do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close end - ensure - stmt.close end end private + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end + def slice_placeholders(values) values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs index a51fa99..460d54e 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -144,12 +144,17 @@ module Sqlite3Codegen end class QuerySql - def initialize: (untyped db) -> void + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> void def transaction: [T] (?mode: (:deferred | :immediate | :exclusive)?) { (instance) -> T } -> T def get_author: (id: Integer) -> GetAuthorRow? + def get_author!: (id: Integer) -> GetAuthorRow + def list_authors: -> Array[ListAuthorsRow] def create_author: (name: String, bio: String?) -> void @@ -182,12 +187,18 @@ module Sqlite3Codegen def many_params: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow? + def many_params!: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> ManyParamsRow + def many_rows: (c0: Integer) -> Array[manyRowsRow] def four_cols: -> Array[FourColsRow] private + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T + def slice_placeholders: (Array[untyped] values) -> String end end diff --git a/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb index 6ad5713..11eaa39 100644 --- a/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb @@ -12,37 +12,63 @@ module Sqlite3Codegen SQL class My_queriesSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def with_numbers2_go(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + with_client('with_numbers2_go') do |client| + query_params = [c0] + stmt = client.prepare(WITH_NUMBERS2_GO_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - nil + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/ruby32_notypes/other_sql.rb b/test/golden/sqlite3/ruby32_notypes/other_sql.rb index 48bd104..677ee1b 100644 --- a/test/golden/sqlite3/ruby32_notypes/other_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/other_sql.rb @@ -25,67 +25,100 @@ class OtherOneRow < Data.define(:c0) SQL class OtherSql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end def no_cols(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(NO_COLS_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('no_cols') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(NO_COLS_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def other_one - client = @db - stmt = client.prepare(OTHER_ONE_SQL) - begin - result = stmt.execute - row = result.first - row && OtherOneRow.new(row[0]) - ensure - stmt.close + with_client('other_one') do |client| + stmt = client.prepare(OTHER_ONE_SQL) + begin + result = stmt.execute + row = result.first + row && OtherOneRow.new(row[0]) + ensure + stmt.close + end end end + # Like #other_one, but raises NotFound when there is no row. + def other_one!(...) + other_one(...) || raise(NotFound, 'OtherOne returned no row') + end + def count_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(COUNT_ROWS_SQL) + with_client('count_rows') do |client| + query_params = [c0] + stmt = client.prepare(COUNT_ROWS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end + end + end + + private + + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin - stmt.execute(*query_params) - client.changes + yield ensure - stmt.close + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } end end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end end end diff --git a/test/golden/sqlite3/ruby32_notypes/query_sql.rb b/test/golden/sqlite3/ruby32_notypes/query_sql.rb index 8cfab6a..a6c17be 100644 --- a/test/golden/sqlite3/ruby32_notypes/query_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -158,291 +158,343 @@ class FourColsRow < Data.define(:c0, :c1, :c2, :c3) end class QuerySql - # @param db [SQLite3::Database] a single connection (SQLite3::Database is not shared across threads) - def initialize(db) + # Raised by the bang variants (get_author!) when the query returns no row. + class NotFound < StandardError; end + + # db: a SQLite3::Database (a single connection, not shared across threads). + # logger: optional, anything with #debug (Logger, Rails.logger); logs the name and duration of every query. + def initialize(db, logger: nil) @db = db + @logger = logger end # Runs the block in a transaction and returns its value: COMMIT when the block returns, ROLLBACK when it # raises. A nested call becomes a SAVEPOINT. mode: :deferred (default), :immediate or :exclusive. def transaction(mode: nil) - outermost = !@db.transaction_active? - savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" - @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") - begin - result = yield self - @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") - result - rescue Exception # rubocop:disable Lint/RescueException - @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") - raise + instrument('transaction') do + outermost = !@db.transaction_active? + savepoint = outermost ? nil : "sqlc_sp_#{rand(0xffff_ffff).to_s(16)}" + @db.execute(outermost ? "BEGIN#{" #{mode.to_s.upcase}" if mode}" : "SAVEPOINT #{savepoint}") + begin + result = yield self + @db.execute(outermost ? 'COMMIT' : "RELEASE SAVEPOINT #{savepoint}") + result + rescue Exception # rubocop:disable Lint/RescueException + @db.execute(outermost ? 'ROLLBACK' : "ROLLBACK TO SAVEPOINT #{savepoint}") + raise + end end end # Returns the author with the given id, or nil when there is none. def get_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(GET_AUTHOR_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && GetAuthorRow.new(row[0], row[1], row[2]) - ensure - stmt.close + with_client('get_author') do |client| + query_params = [id] + stmt = client.prepare(GET_AUTHOR_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && GetAuthorRow.new(row[0], row[1], row[2]) + ensure + stmt.close + end end end + # Like #get_author, but raises NotFound when there is no row. + def get_author!(...) + get_author(...) || raise(NotFound, 'GetAuthor returned no row') + end + def list_authors - client = @db - stmt = client.prepare(LIST_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAuthorsRow.new(row[0], row[1], row[2]) + with_client('list_authors') do |client| + stmt = client.prepare(LIST_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAuthorsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('create_author') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def delete_author(id:) - client = @db - query_params = [id] - stmt = client.prepare(DELETE_AUTHOR_SQL) - begin - stmt.execute(*query_params) - nil - ensure - stmt.close + with_client('delete_author') do |client| + query_params = [id] + stmt = client.prepare(DELETE_AUTHOR_SQL) + begin + stmt.execute(*query_params) + nil + ensure + stmt.close + end end end def list_authors_paged(offset:, limit:) - client = @db - query_params = [offset, limit] - stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - ListAuthorsPagedRow.new(row[0], row[1], row[2]) + with_client('list_authors_paged') do |client| + query_params = [offset, limit] + stmt = client.prepare(LIST_AUTHORS_PAGED_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + ListAuthorsPagedRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_author_by_name_pattern(name_pattern: nil) - client = @db - query_params = [name_pattern] - stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + with_client('get_author_by_name_pattern') do |client| + query_params = [name_pattern] + stmt = client.prepare(GET_AUTHOR_BY_NAME_PATTERN_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorByNamePatternRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def create_author_return_id(name:, bio:) - client = @db - query_params = [name, bio] - stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_author_return_id') do |client| + query_params = [name, bio] + stmt = client.prepare(CREATE_AUTHOR_RETURN_ID_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end # Sets the bio of every author that has one and returns how many were changed. def update_author_bios(bio:) - client = @db - query_params = [bio] - stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) - begin - stmt.execute(*query_params) - client.changes - ensure - stmt.close + with_client('update_author_bios') do |client| + query_params = [bio] + stmt = client.prepare(UPDATE_AUTHOR_BIOS_SQL) + begin + stmt.execute(*query_params) + client.changes + ensure + stmt.close + end end end def get_authors_by_ids(ids:) - client = @db - query_params = [*ids] - sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids') do |client| + query_params = [*ids] + sql = GET_AUTHORS_BY_IDS_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_ids_and_names(ids:, names:) - client = @db - query_params = [*ids, *names] - sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) - sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) - stmt = client.prepare(sql) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + with_client('get_authors_by_ids_and_names') do |client| + query_params = [*ids, *names] + sql = GET_AUTHORS_BY_IDS_AND_NAMES_SQL.sub('/*SLICE:ids*/?', slice_placeholders(ids)) + sql = sql.sub('/*SLICE:names*/?', slice_placeholders(names)) + stmt = client.prepare(sql) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByIdsAndNamesRow.new(row[0], row[1], row[2]) + end + ensure + stmt.close end - ensure - stmt.close end end # Bulk insert: pass an Array of CopyAuthorsArgs. def copy_authors(rows) - client = @db - rows.each_slice(499) do |batch| - sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" - client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + with_client('copy_authors') do |client| + rows.each_slice(499) do |batch| + sql = "INSERT INTO authors (name, bio) VALUES #{Array.new(batch.size, '(?, ?)').join(', ')}" + client.execute(sql, batch.flat_map { |row| [row.name, row.bio] }) + end + nil end - nil end def truncate_authors - client = @db - stmt = client.prepare(TRUNCATE_AUTHORS_SQL) - begin - stmt.execute - nil - ensure - stmt.close + with_client('truncate_authors') do |client| + stmt = client.prepare(TRUNCATE_AUTHORS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end end end def create_book(name:, author_id:) - client = @db - query_params = [name, author_id] - stmt = client.prepare(CREATE_BOOK_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && row[0] - ensure - stmt.close + with_client('create_book') do |client| + query_params = [name, author_id] + stmt = client.prepare(CREATE_BOOK_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && row[0] + ensure + stmt.close + end end end def list_all_authors_books - client = @db - stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) - begin - result = stmt.execute - result.map do |row| - ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + with_client('list_all_authors_books') do |client| + stmt = client.prepare(LIST_ALL_AUTHORS_BOOKS_SQL) + begin + result = stmt.execute + result.map do |row| + ListAllAuthorsBooksRow.new(Author.new(row[0], row[1], row[2]), Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_duplicate_authors - client = @db - stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) - begin - result = stmt.execute - result.map do |row| - GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + with_client('get_duplicate_authors') do |client| + stmt = client.prepare(GET_DUPLICATE_AUTHORS_SQL) + begin + result = stmt.execute + result.map do |row| + GetDuplicateAuthorsRow.new(Author.new(row[0], row[1], row[2]), Author.new(row[3], row[4], row[5])) + end + ensure + stmt.close end - ensure - stmt.close end end def get_authors_by_book_name(name:) - client = @db - query_params = [name] - stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + with_client('get_authors_by_book_name') do |client| + query_params = [name] + stmt = client.prepare(GET_AUTHORS_BY_BOOK_NAME_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + GetAuthorsByBookNameRow.new(row[0], row[1], row[2], Book.new(row[3], row[4], row[5], row[6])) + end + ensure + stmt.close end - ensure - stmt.close end end def many_params(c0:, c1:, c2:, c3:, c4:) - client = @db - query_params = [ - c0, - c1, - c2, - c3, - c4 - ] - stmt = client.prepare(MANY_PARAMS_SQL) - begin - result = stmt.execute(*query_params) - row = result.first - row && ManyParamsRow.new(row[0], row[1]) - ensure - stmt.close + with_client('many_params') do |client| + query_params = [ + c0, + c1, + c2, + c3, + c4 + ] + stmt = client.prepare(MANY_PARAMS_SQL) + begin + result = stmt.execute(*query_params) + row = result.first + row && ManyParamsRow.new(row[0], row[1]) + ensure + stmt.close + end end end + # Like #many_params, but raises NotFound when there is no row. + def many_params!(...) + many_params(...) || raise(NotFound, 'ManyParams returned no row') + end + def many_rows(c0:) - client = @db - query_params = [c0] - stmt = client.prepare(MANY_ROWS_SQL) - begin - result = stmt.execute(*query_params) - result.map do |row| - manyRowsRow.new( - row[0], - row[1], - row[2], - row[3], - row[4] - ) + with_client('many_rows') do |client| + query_params = [c0] + stmt = client.prepare(MANY_ROWS_SQL) + begin + result = stmt.execute(*query_params) + result.map do |row| + manyRowsRow.new( + row[0], + row[1], + row[2], + row[3], + row[4] + ) + end + ensure + stmt.close end - ensure - stmt.close end end def four_cols - client = @db - stmt = client.prepare(FOUR_COLS_SQL) - begin - result = stmt.execute - result.map do |row| - FourColsRow.new(row[0], row[1], row[2], row[3]) + with_client('four_cols') do |client| + stmt = client.prepare(FOUR_COLS_SQL) + begin + result = stmt.execute + result.map do |row| + FourColsRow.new(row[0], row[1], row[2], row[3]) + end + ensure + stmt.close end - ensure - stmt.close end end private + def instrument(name) + return yield unless @logger + + started = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + yield + ensure + elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000 + @logger.debug { format('%s#%s (%.1fms)', class: self.class.name, name: name, ms: elapsed) } + end + end + + def with_client(name, &block) + instrument(name) { block.call(@db) } + end + def slice_placeholders(values) values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ') end diff --git a/tests/end2end_mysql2.rb b/tests/end2end_mysql2.rb index e65efa9..8f038cd 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -1,6 +1,8 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'logger' +require 'stringio' require 'async' require 'async/barrier' require_relative 'consts' @@ -317,6 +319,26 @@ def test_queries_run_concurrently_under_the_fiber_scheduler assert_equal 10, @query_sql.list_authors.size end + # ---- Ruby conventions: bang finders, logger ---------------------------------------------------------------- + + def test_bang_finder_raises_not_found + id = create_author('a', nil) + assert_equal 'a', @query_sql.get_author!(id: id).name + error = assert_raises(QuerySql::NotFound) { @query_sql.get_author!(id: -1) } + assert_equal 'GetAuthor returned no row', error.message + assert_nil @query_sql.get_author(id: -1) # the plain finder still returns nil + end + + def test_logger_receives_query_names_and_durations + io = StringIO.new + logged = QuerySql.connect(mysql2_params, logger: Logger.new(io, level: Logger::DEBUG)) + logged.list_authors + logged.transaction { |tx| tx.list_authors } + assert_match(/QuerySql#list_authors \(\d+\.\dms\)/, io.string) + assert_match(/QuerySql#transaction \(\d+\.\dms\)/, io.string) + @query_sql.list_authors # no logger: nothing is logged, nothing breaks + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index 8f33023..574c789 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -1,6 +1,8 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'logger' +require 'stringio' require 'async' require 'async/barrier' require 'bigdecimal' @@ -388,6 +390,26 @@ def test_queries_run_concurrently_under_the_fiber_scheduler assert_equal 10, @query_sql.list_authors.size end + # ---- Ruby conventions: bang finders, logger ---------------------------------------------------------------- + + def test_bang_finder_raises_not_found + id = create_author('a', nil) + assert_equal 'a', @query_sql.get_author!(id: id).name + error = assert_raises(QuerySql::NotFound) { @query_sql.get_author!(id: -1) } + assert_equal 'GetAuthor returned no row', error.message + assert_nil @query_sql.get_author(id: -1) # the plain finder still returns nil + end + + def test_logger_receives_query_names_and_durations + io = StringIO.new + logged = QuerySql.connect(pg_params, logger: Logger.new(io, level: Logger::DEBUG)) + logged.list_authors + logged.transaction { |tx| tx.list_authors } + assert_match(/QuerySql#list_authors \(\d+\.\dms\)/, io.string) + assert_match(/QuerySql#transaction \(\d+\.\dms\)/, io.string) + @query_sql.list_authors # no logger: nothing is logged, nothing breaks + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table diff --git a/tests/end2end_sqlite3.rb b/tests/end2end_sqlite3.rb index 6bee695..5096ca1 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -1,6 +1,8 @@ #!/usr/bin/env ruby gem 'minitest' # ensures using the gem, and not the built-in MT require 'minitest/autorun' +require 'logger' +require 'stringio' require 'sqlite3' require_relative 'consts' require_relative 'rbs_types' @@ -215,6 +217,26 @@ def test_transaction_mode assert_equal 1, @query_sql.transaction(mode: :immediate) { 1 } end + # ---- Ruby conventions: bang finders, logger ---------------------------------------------------------------- + + def test_bang_finder_raises_not_found + id = create_author('a', nil) + assert_equal 'a', @query_sql.get_author!(id: id).name + error = assert_raises(QuerySql::NotFound) { @query_sql.get_author!(id: -1) } + assert_equal 'GetAuthor returned no row', error.message + assert_nil @query_sql.get_author(id: -1) # the plain finder still returns nil + end + + def test_logger_receives_query_names_and_durations + io = StringIO.new + logged = QuerySql.new(@db, logger: Logger.new(io, level: Logger::DEBUG)) + logged.list_authors + logged.transaction { |tx| tx.list_authors } + assert_match(/QuerySql#list_authors \(\d+\.\dms\)/, io.string) + assert_match(/QuerySql#transaction \(\d+\.\dms\)/, io.string) + @query_sql.list_authors # no logger: nothing is logged, nothing breaks + end + # ---- models ---------------------------------------------------------------------------------------------------- def test_models_are_generated_for_every_table From 9a8fe7dac65c574e5cf163f89fcd13e6ba3f75db Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 17:07:17 +0000 Subject: [PATCH 15/15] Add architecture/testing/releasing docs and CLAUDE.md; replace the commit-message release trigger with a manual Release workflow Docs: Architecture (how the generator works, adding features/drivers/options), Testing (layers + CI), Releasing (process + versioning); Design.md loses its stale C#-parity sections; README links the docs and fixes the badges. Release: workflow_dispatch (bump / explicit version / dry run / draft). It runs build.yml and tests.yml on the exact commit (now workflow_call-able), publishes the tested plugin.wasm with its sha256 and generated notes, then verifies the public download with `sqlc diff`. scripts/releasing/next_version.sh computes the version from the latest tag. Replaces the "[release]" commit-message trigger. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01FskpZBmVEG9iLWAWhrBiRo --- .github/pull_request_template.md | 2 +- .github/release.yml | 16 ++ .github/workflows/build.yml | 3 +- .github/workflows/release.yml | 257 ++++++++++++++++-------------- .github/workflows/tests.yml | 3 +- CLAUDE.md | 44 +++++ README.md | 28 ++-- docs/Architecture.md | 85 ++++++++++ docs/Design.md | 38 ++--- docs/Releasing.md | 59 +++++++ docs/Testing.md | 51 ++++++ scripts/releasing/next_version.sh | 32 ++++ 12 files changed, 454 insertions(+), 164 deletions(-) create mode 100644 .github/release.yml create mode 100644 CLAUDE.md create mode 100644 docs/Architecture.md create mode 100644 docs/Releasing.md create mode 100644 docs/Testing.md create mode 100755 scripts/releasing/next_version.sh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b60a608..bf67ecb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,4 +5,4 @@ - [ ] `examples/` is regenerated (`rake sqlc_generate_process`) and committed - [ ] Tests added or updated (`rake test`, and `rake end2end_*` when the generated code changes) - [ ] The generated code stays RuboCop-clean (`rake rubocop`) and Steep-clean (`rake steep`) -- [ ] `[release]` / `[minor]` / `[major]` in the merge commit message only if this should create a release +- [ ] Labelled (`feature`, `bug`, `breaking`, `documentation`) so the release notes are grouped correctly diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..ab22468 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,16 @@ +# Categories for the notes GitHub generates for a release (labels of the merged pull requests). +changelog: + exclude: + labels: [skip-changelog] + authors: [dependabot] + categories: + - title: Breaking changes (generated code or options changed incompatibly) + labels: [breaking] + - title: New features + labels: [enhancement, feature] + - title: Bug fixes + labels: [bug] + - title: Documentation + labels: [documentation] + - title: Other changes + labels: ["*"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bedf59..7bb809c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,10 +7,11 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: build-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: + workflow_call: # the Release workflow runs the full gate on the exact commit it releases push: branches: [ "main" ] pull_request: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb20011..360eecd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,145 +1,160 @@ name: Release + +# A release is a decision, so it is started by hand: Actions -> Release -> Run workflow (or `gh workflow run release.yml`). +# Everything after that is automatic: the full test gate runs on the commit, the tag + GitHub release are created +# with the exact plugin.wasm that was tested, and the published asset is verified by running sqlc with it. +on: + workflow_dispatch: + inputs: + bump: + description: Which part of the version to bump (ignored when 'version' is set) + type: choice + options: [patch, minor, major] + default: patch + version: + description: "Explicit version instead of a bump, e.g. v1.0.0 (optional)" + type: string + required: false + dry_run: + description: Only compute the version and the release notes, publish nothing + type: boolean + default: false + draft: + description: Create a draft release that you publish by hand + type: boolean + default: false + defaults: run: shell: bash permissions: - contents: write - pull-requests: write + contents: read + +concurrency: + group: release + cancel-in-progress: false -on: - workflow_run: - workflows: [Build] - types: [completed] - branches: [main] - jobs: - check_commit: - name: Check Commit + plan: + name: Plan runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' # releases are cut from main only outputs: - IS_RELEASE: ${{ steps.check_msg.outputs.IS_RELEASE }} + version: ${{ steps.next.outputs.version }} + previous: ${{ steps.next.outputs.previous }} steps: - - name: Check out Git repository - uses: actions/checkout@v7 + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # all tags - - name: Check latest commit message - id: check_msg + - name: Compute the next version + id: next + env: + BUMP: ${{ inputs.bump }} + EXPLICIT_VERSION: ${{ inputs.version }} run: | - LATEST_COMMIT_MSG=$(git log -1 --pretty=%B | cat) - if [[ ${LATEST_COMMIT_MSG} == *"[release]"* ]]; then - echo "release message found - continuing with the release" - IS_RELEASE="true" - else - echo "no release message found - skipping..." - IS_RELEASE="false" - fi - echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT - + version=$(./scripts/releasing/next_version.sh "${BUMP}" "${EXPLICIT_VERSION}") + previous=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n 1) + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "previous=${previous}" >> "$GITHUB_OUTPUT" + echo "### Releasing ${version} (previous: ${previous:-none}) from \`${GITHUB_SHA::7}\`" >> "$GITHUB_STEP_SUMMARY" + + build: + name: Build + needs: [plan] + uses: ./.github/workflows/build.yml + + tests: + name: Tests + needs: [plan] + uses: ./.github/workflows/tests.yml + release: - name: Release - needs: [check_commit] + name: Release ${{ needs.plan.outputs.version }} + needs: [plan, build, tests] runs-on: ubuntu-latest - if: ${{ needs.check_commit.outputs.IS_RELEASE == 'true' }} + permissions: + contents: write # tag + release env: - IS_RELEASE: ${{ needs.check_commit.outputs.IS_RELEASE }} + VERSION: ${{ needs.plan.outputs.version }} + PREVIOUS: ${{ needs.plan.outputs.previous }} + GH_TOKEN: ${{ github.token }} + outputs: + sha256: ${{ steps.asset.outputs.sha256 }} steps: - - name: Check out Git repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Download artifact - id: download-artifact - uses: dawidd6/action-download-artifact@v6 + - uses: actions/checkout@v7 + + # the very plugin.wasm the Build + Tests jobs of this run verified + - uses: actions/download-artifact@v8 with: - github_token: ${{secrets.GITHUB_TOKEN}} - workflow: build.yml - workflow_conclusion: success name: wasm-file - - - name: Bump version + path: dist + + - name: Prepare the release asset + id: asset run: | - set -e - LATEST_COMMIT_MSG=$(git log -1 --pretty=%B | cat) - - echo "Extract the latest tag version" - LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) - - BUMP_TYPE="patch" # Initialize the default version bump type to patch - if [[ "${LATEST_COMMIT_MSG}" == *"[major]"* ]]; then - BUMP_TYPE="major" - elif [[ "${LATEST_COMMIT_MSG}" == *"[minor]"* ]]; then - BUMP_TYPE="minor" - fi - - case ${BUMP_TYPE} in - "major") - NEW_TAG=$(echo "${LATEST_TAG}" | awk -F. '{OFS="."; $1="v" substr($1,2)+1; $2="0"; $3="0"; print}') - ;; - "minor") - NEW_TAG=$(echo "${LATEST_TAG}" | awk -F. '{OFS="."; $2=$2+1; $3="0"; print}') - ;; - "patch") - NEW_TAG=$(echo "${LATEST_TAG}" | awk -F. '{OFS="."; $3=$3+1; print}') - ;; - esac - - echo "LATEST_TAG=${LATEST_TAG}" >> $GITHUB_ENV - echo "NEW_TAG=${NEW_TAG}" >> $GITHUB_ENV - - - name: Push tag + mkdir release + cp dist/plugin.wasm release/sqlc-gen-ruby.wasm + sha256=$(sha256sum release/sqlc-gen-ruby.wasm | awk '{ print $1 }') + echo "${sha256} sqlc-gen-ruby.wasm" > release/sqlc-gen-ruby.wasm.sha256 + echo "sha256=${sha256}" >> "$GITHUB_OUTPUT" + + - name: Write the release notes + env: + SHA256: ${{ steps.asset.outputs.sha256 }} run: | - NEW_TAG=${{ env.NEW_TAG }} - - git tag ${NEW_TAG} - git push origin ${NEW_TAG} - - - name: Calculate sha256 + generated=$(gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="${VERSION}" -f target_commitish="${GITHUB_SHA}" \ + ${PREVIOUS:+-f previous_tag_name="${PREVIOUS}"} --jq .body) + { + echo "Plugin sha256: \`${SHA256}\`" + echo + echo "## Usage" + echo '```yaml' + echo "version: '2'" + echo "plugins:" + echo "- name: ruby" + echo " wasm:" + echo " url: https://github.com/${GITHUB_REPOSITORY}/releases/download/${VERSION}/sqlc-gen-ruby.wasm" + echo " sha256: ${SHA256}" + echo '```' + echo + echo "${generated}" + } > release/notes.md + cat release/notes.md >> "$GITHUB_STEP_SUMMARY" + + - name: Create the release (tag ${{ env.VERSION }}) + if: ${{ !inputs.dry_run }} run: | - SHA256_HASH=$(sha256sum plugin.wasm | awk '{ print $1 }') - echo "SHA256_HASH=${SHA256_HASH}" >> $GITHUB_ENV - echo "The calculated sha256 is $SHA256_HASH" + gh release create "${VERSION}" \ + release/sqlc-gen-ruby.wasm release/sqlc-gen-ruby.wasm.sha256 \ + --target "${GITHUB_SHA}" \ + --title "${VERSION}" \ + --notes-file release/notes.md \ + ${{ inputs.draft && '--draft' || '' }} - - name: Create release draft - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # a user's view of the release: download it by its public URL, check the checksum and generate code with sqlc + verify: + name: Verify the published release + needs: [plan, release] + if: ${{ !inputs.dry_run && !inputs.draft }} + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.plan.outputs.version }} + SHA256: ${{ needs.release.outputs.sha256 }} + steps: + - uses: actions/checkout@v7 + + - name: Load tool versions + run: grep -E '^[A-Z_]+=' scripts/tool-versions.env >> "$GITHUB_ENV" + + - uses: sqlc-dev/setup-sqlc@v5 + with: + sqlc-version: '${{ env.SQLC_VERSION }}' + + - name: sqlc diff with the released plugin run: | - set -e - - LATEST_TAG=${{ env.LATEST_TAG }} - NEW_TAG=${{ env.NEW_TAG }} - SHA256_HASH=${{ env.SHA256_HASH }} - - CHANGE_LOG=$(git --no-pager log ${LATEST_TAG}..HEAD --pretty=format:'%h - %an, %ar : %s') - - # Define the release notes template - RELEASE_NOTES=$(cat < sqlc.release.yaml + sqlc -f sqlc.release.yaml diff diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c9889c9..c1d5f14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,10 +7,11 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: tests-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} on: + workflow_call: # the Release workflow runs the full gate on the exact commit it releases push: branches: [ "main" ] pull_request: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..37e1a6b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# CLAUDE.md + +sqlc-gen-ruby: a [sqlc](https://sqlc.dev) plugin, written in pure Ruby, that generates Ruby data-access code (+ RBS) +for `pg`, `mysql2` and `sqlite3`. It ships as one WASM module (the ruby.wasm interpreter with the plugin embedded). +Docs: [Architecture](docs/Architecture.md) · [Testing](docs/Testing.md) · [Releasing](docs/Releasing.md) · [Design](docs/Design.md). + +## Commands +``` +bundle exec rake test # unit + golden tests, no database (run this constantly) +bundle exec rake rubocop steep # the GENERATED code in examples/ must stay clean +rake sqlc_generate_process # regenerate examples/ with exe/plugin.rb (needs sqlc >= scripts/tool-versions.env) +rake sqlc_generate_wasm # same through the real WASM (slow: builds dist/plugin.wasm) +rake end2end_sqlite3 # E2E of the generated code, no server; end2end_pg / end2end_mysql2 need servers +UPDATE_GOLDEN=1 bundle exec ruby -Itest test/extended_golden_test.rb # rewrite test/golden after an intended change +``` + +## Where things are +* `lib/sqlc_gen_ruby/` – the plugin: `generator.rb` (files), `method_gen.rb` (one method per query), `drivers/*.rb` + (everything database-specific), `type_resolver.rb`, `row_layout.rb`, `options.rb`, `proto.rb` (own protobuf codec). +* `examples/authors//{schema,query}.sql` → input; `examples//` → **generated output, committed, never edit by hand**. +* `test/` unit + golden (+ `fixtures/*.bin`, real sqlc requests); `tests/` end-to-end against databases. +* `scripts/` build (`wasm/`), test helpers (`tests/`), release (`releasing/`), docs generation (`docs/`). + +## Rules that are easy to break +* **Changing generator output means regenerating `examples/`** (`rake sqlc_generate_process`) and the golden files in + the same commit; CI runs `sqlc diff` and fails on drift. Read the diff of generated files before committing. +* **Driver differences live in `drivers/*.rb` hooks, not in `if driver ==` inside `method_gen.rb`.** +* **The plugin runs inside ruby.wasm**: only Ruby core + `json`; no gems, no C extensions, no filesystem, no network. + Anything `require`d by `lib/` or `exe/` must exist in the WASM (`scripts/wasm/build.sh` stages the stdlib). Gems used + only by tests/tooling go in the `Gemfile` groups, never in `lib/`. +* **Generated code must stay valid for Ruby 3.1–3.3** (`Struct` below 3.2, `Data` from 3.2) and RuboCop/Steep-clean. +* Generated code is plain, idiomatic Ruby (keyword args, `Data` rows, implicit returns, 2-space indent); do not mirror + the C# sibling plugin's style. Parameter *values* are never logged. +* Prepared statements are per connection (`ConnectionState`) because the query object is shared by pooled connections. +* sqlc runs process plugins with an empty env (`env: [PATH]` is needed in `sqlc.local.yaml`) and resolves `out:` + relative to the config file. + +## Git, CI and releases +* Develop on a feature branch, small commits; PRs target `main`. Label PRs (`feature`, `bug`, `breaking`, `documentation`). +* CI: `build.yml` (WASM, lint, `sqlc diff`, Steep, option matrix) and `tests.yml` (unit, Ruby 3.1/3.2/3.4, E2E on real + PostgreSQL/MySQL/SQLite). Both must be green. +* **Never create tags or releases by hand and never add release markers to commit messages.** Releases are started by a + maintainer with the *Release* workflow (`docs/Releasing.md`); do not trigger it unless explicitly asked. +* Tool versions (sqlc, ruby.wasm) are pinned in `scripts/tool-versions.env`; bump them there only. diff --git a/README.md b/README.md index 48342fd..b2c2f3a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![CI](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/ci.yml) +[![Build](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/build.yml) +[![Tests](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/DaredevilOSS/sqlc-gen-ruby/actions/workflows/tests.yml) # sqlc-gen-ruby A [sqlc](https://sqlc.dev) plugin that generates Ruby code (plus optional RBS type signatures) from SQL. @@ -6,7 +7,7 @@ Supported drivers: `pg`, `mysql2` and `sqlite3`. The plugin itself is written in Ruby: it is pure Ruby with no native dependencies (it has its own tiny protobuf codec), and it is shipped as a single self-contained WebAssembly module — the Ruby interpreter -([ruby.wasm](https://github.com/ruby/ruby.wasm)) with the plugin embedded. See [docs/Design.md](docs/Design.md). +([ruby.wasm](https://github.com/ruby/ruby.wasm)) with the plugin embedded. See [How it works](#documentation). ## Usage @@ -184,15 +185,16 @@ To refresh a fixture see `scripts/capture_request.rb`. * Tool versions live in `scripts/tool-versions.env`; dependabot keeps actions and gems current. ## Release flow -The release flow in this repo follows the semver conventions, building tag as `v[major].[minor].[patch]`. +Releases are started by hand and fully automated after that: **Actions → Release → Run workflow** (patch / minor / major, +optional dry run). The workflow runs the whole test gate on the commit, then tags, publishes the GitHub release with the +tested `sqlc-gen-ruby.wasm` and its sha256, and verifies the published file with sqlc. Details: [docs/Releasing.md](docs/Releasing.md). -* In order to create a release you need to add `[release]` somewhere in your commit message when merging to master - -### Version bumping (built on tags) -**By default, the release script will bump the patch version.**, by adding `[release]` to your commit message the release script will create a new tag with `v[major].[minor].[patch]+1`. -* Bump `minor` version by adding `[minor]` to your commit message resulting in a new tag with `v[major].[minor]+1.0`
-* Bump `major` version by adding `[major]` to your commit message resulting in a new tag with `v[major]+1.0.0` - -### Release structure -The new created tag will create a draft release with it, in the release there will be the wasm plugin embedded in the release.
-> Publish the draft +## Documentation +| Doc | What | +|---|---| +| [docs/Architecture.md](docs/Architecture.md) | how the generator works, where to change what, adding a feature / driver / option | +| [docs/Testing.md](docs/Testing.md) | the test layers, CI workflows, what to run before pushing | +| [docs/Releasing.md](docs/Releasing.md) | the release process and versioning rules | +| [docs/Design.md](docs/Design.md) | the WASM packaging, design decisions, known limitations | +| [docs/Examples.md](docs/Examples.md) | per-driver configs and links to the generated examples | +| [CLAUDE.md](CLAUDE.md) | the same orientation condensed for AI coding assistants | diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 0000000..381b96b --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1,85 @@ +# Architecture: how the generator works + +A sqlc plugin is a program that gets a protobuf `GenerateRequest` on stdin (the parsed schema, the queries, the +plugin's `options:`) and writes a `GenerateResponse` (a list of files) to stdout. This plugin does exactly that in +pure Ruby; [Design.md](Design.md) explains how it is packed into a WASM module. + +``` +stdin ─► Proto (decode) ─► Generator ─┬─► Options / RubyVersion what the user asked for + ├─► Catalog tables + enums ─► models.rb + ├─► RowLayout + TypeResolver result columns ─► Row attributes + RBS types + ├─► Drivers::{Pg,Mysql2,Sqlite3} everything that differs per database library + └─► MethodGen one Ruby method (+ RBS) per query + files ◄─────┘ +stdout ◄─ Proto (encode) ◄─ GenerateResponse (query_sql.rb, query_sql.rbs, models.rb, connection.rb, Gemfile, ...) +``` + +## The pieces + +| Piece | Responsibility | +|---|---| +| `exe/plugin.rb` | entry point. Errors go to stderr with exit code 1 (sqlc shows them); `SQLC_GEN_RUBY_DEBUG=1` adds a backtrace | +| `proto.rb`, `plugin_pb.rb` | a ~150 line protobuf wire codec and the subset of sqlc's `codegen.proto` we read. Field numbers must match upstream. `google-protobuf` is a C extension and cannot run inside ruby.wasm | +| `options.rb`, `ruby_version.rb` | parse + validate the `options:` JSON (`driver`, `rubyVersion`, `overrides`, ...) | +| `generator.rb` | the orchestrator: one file per query file (`query_sql.rb`), plus `models.rb`, `connection.rb`, `Gemfile`; builds the module / class / `Data` skeleton | +| `method_gen.rb` | the code of one query: signature (keyword arguments), SQL constant, command (`:one`, `:many`, `:exec`, `:execrows`, `:execlastid`, `:copyfrom`), `sqlc.slice`, bang finder, and the RBS signature | +| `row_layout.rb` | turns result columns into row attributes; `sqlc.embed` collapses a table's columns into one model attribute | +| `type_resolver.rb` | RBS type of a column / parameter: driver type table, arrays, nullability (`T?`), `overrides:` | +| `catalog.rb` | the schema's tables and enums, and their Ruby names | +| `drivers/base.rb` | the driver interface and what all drivers share: `instrument` logging, `NotFound`, transaction RBS | +| `drivers/{pg,mysql2,sqlite3}.rb` | the per-library parts: type mapping, connecting / pooling, preparing and executing statements, transactions, `:copyfrom` | +| `code.rb` | text helpers: indentation, `snake_case`, `camelize`, Ruby-keyword-safe parameter names | + +**Rule of thumb:** `MethodGen` / `Generator` decide *what* is generated and are the same for every database; a driver only +supplies *snippets* (how to prepare a statement, how to read the affected rows, ...). If you find a `case driver` in +`MethodGen`, it belongs in a driver hook instead. + +## What the generated code looks like + +```ruby +module PgCodegen + class QuerySql + GET_AUTHOR_SQL = <<~'SQL' # raw heredoc: the SQL is never interpolated + SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 + SQL + GetAuthorRow = Data.define(:id, :name, :bio) # Struct.new(...) below Ruby 3.2 + + def get_author(id:) # a :one query returns the row or nil ... + with_client('get_author') do |client| # one pooled connection + logging + ...prepare once per connection, exec_prepared... + end + end + + def get_author!(id:) # ... and gets a bang variant + get_author(id: id) || raise(NotFound, 'GetAuthor returned no row') + end + + def transaction(isolation: nil) { |tx| ... } # commit / rollback, nested = SAVEPOINT + end +end +``` +Look at [`examples/pg/query_sql.rb`](../examples/pg/query_sql.rb) for a complete file; it is the plugin's real output. + +Design decisions worth knowing (the *why* is in the comments of `drivers/base.rb` and `drivers/pg.rb`): + +* **Prepared statements are per connection.** A pooled query object is shared by many connections, and a prepared + statement exists only on the connection that prepared it, so each connection remembers its own (`ConnectionState`). +* **Pool injection.** `QuerySql.new(pool)` accepts anything that responds to `#with`, so an app can share its pool + and tests can pass a fake. `connect(...)` is only a convenience that builds a `connection_pool`. +* **Statements are closed.** SQLite/MySQL statements are closed in `ensure` (an open SQLite statement blocks `COMMIT`). +* **Nothing from the plugin leaks into the app.** The generated code only requires the driver gem and `connection_pool`. + +## Adding things + +**A new query feature** (an annotation or macro): +1. read what sqlc sends in a real request: `scripts/capture_request.rb` saves one to `test/fixtures/` (see [Testing](Testing.md)); +2. handle it in `MethodGen` (add a driver hook in `Base` if the code differs per database); +3. add a query using it to `examples/authors/*/{schema,query}.sql` for every driver that can run it; +4. `rake sqlc_generate_process`, review the diff of `examples/`, update the golden files, add unit and end-to-end tests. + +**A new driver** (e.g. `trilogy`): subclass `Drivers::Base`, implement the abstract hooks (the abstract ones raise +`NotImplementedError`), register it in `Generator#instantiate_driver` and `Options::DRIVERS`, add `examples/authors/` + an end-to-end test file `tests/end2end_.rb`, and a service +container job in `.github/workflows/tests.yml`. + +**A new option:** `options.rb` (parse + default), use it where needed, document it in the README option table, and +add a case to `test/options_test.rb` and (if it changes output) to the `codegen-options` matrix in `build.yml`. diff --git a/docs/Design.md b/docs/Design.md index 20a9258..25373fd 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -16,7 +16,7 @@ process or as a WASI module. This plugin is pure Ruby and is distributed as a WA | `lib/sqlc_gen_ruby/row_layout.rb` | how result columns become `Row` attributes (`sqlc.embed` expands a table into one model attribute) | | `lib/sqlc_gen_ruby/type_resolver.rb`, `catalog.rb` | RBS type of a column (DB type mapping + array + nullability + `overrides:`), tables / enums of the catalog | | `lib/sqlc_gen_ruby/drivers/*.rb` | `Base` + `Pg`, `Mysql2`, `Sqlite3`: column type mapping, statement preparation / execution, pooling, row access | -| `lib/sqlc_gen_ruby/code.rb` | indent / snake_case helpers (formatting must stay byte-identical with the original implementation) | +| `lib/sqlc_gen_ruby/code.rb` | indent / snake_case helpers | Driver differences are data, not copy/paste: `Base` has `pooling_enabled?` (ConnectionPool vs a single injected connection), `row_data_type` (`row['col']` vs `row[0]`), `supports_exec_last_id?` (mysql2 only) and a column mapping @@ -33,18 +33,9 @@ table (exact or prefix based matching). **The argv trick:** sqlc runs the module as `plugin.wasm /plugin.CodegenService/Generate`, so the interpreter treats that path as its script. The build embeds a file at exactly that path containing `load '/app/exe/plugin.rb'`. -## Verification strategy - -The previous C# implementation defined the expected behaviour. Its output is kept as test data: - -* `examples/{pg,mysql2,sqlite3}` — checked-in output of the C# plugin for the example schemas; -* `test/golden/*` — C# output for option variants (Ruby 3.1 `Struct`, 3.2 / no types, `generateGemfile`) over the sqlc - captured queries plus synthetic ones (5+ params / columns for the multi-line formatting, lower-case names, several - query files); -* `test/fixtures/*.bin` — real `GenerateRequest`s captured from `sqlc generate` (v1.27.0). - -`test/codec_test.rb` cross-checks the hand-written protobuf codec against Google's official implementation (test-only -dependency). +## Verification +How the plugin is tested (golden files from real sqlc requests, `sqlc diff` over `examples/`, end-to-end tests +against real databases) is described in [Testing.md](Testing.md); how it works inside in [Architecture.md](Architecture.md). ## What the generated code looks like (and why) Generated code follows what Ruby code usually looks like rather than mirroring the C# plugin: keyword arguments instead of @@ -53,22 +44,15 @@ header, SQL in raw heredocs, `connection_pool` for pooling with an injectable po Prepared statements are named per query class and remembered per *connection* (a module mixed into each connection), so several query objects can share a pool. -## Beyond the C# port -The first version of the Ruby plugin reproduced the C# plugin's output exactly (verified against the C# binary, see git -history). It has since gained the sqlc features of the sibling `sqlc-gen-csharp` plugin (`:execrows`, `:execlastid` for -every driver, `:copyfrom`, `sqlc.slice`, `sqlc.embed`, enums, models, transactions, nullability, `overrides`) and -its type tables were corrected to the classes the drivers really return, so the output is no longer identical. -`tests/end2end_*.rb` run the generated code against real databases; `tests/rbs_types.rb` checks returned values -against the generated signatures. +## History +The plugin started as a port of the C# `sqlc-gen-csharp` plugin and reproduced its output byte for byte (verified +against the C# binary; see the git history before the commit that removed the C# code). It has since gained the sqlc features of +that plugin (`:execrows`, `:execlastid` for every driver, `:copyfrom`, `sqlc.slice`, `sqlc.embed`, enums, models, +transactions, nullability, `overrides`) and Ruby conventions (bang finders, logger, pooling), so the output is +now idiomatic Ruby rather than a copy of the C# one. ## Known limitations -* `%q(...)` SQL constants break if the query text has unbalanced parentheses (inside a string literal). * The SQLite `NUMERIC` affinity can return Integers where `Float` is declared. * `:copyfrom` for MySQL writes binary (`BLOB`) values through a text CSV; only text-safe values round-trip. * The generated query classes are not thread-safe for SQLite (a single connection is injected). - -## Known quirks kept from the C# implementation -* With Ruby < 3.2 the generated `Struct.new(id, name)` lists bare identifiers, not symbols (`:id, :name`). - This is what the C# plugin generated, so it is preserved. -* `3.3.x` (a patch version) is rejected: the original version comparison treats a patch as greater than "no patch". -* An unknown `driver` is an error here (the C# enum parse silently fell back to `mysql2`). +* `3.3.x` (a patch version) is rejected as `rubyVersion`; use `3.3`. diff --git a/docs/Releasing.md b/docs/Releasing.md new file mode 100644 index 0000000..c625f7b --- /dev/null +++ b/docs/Releasing.md @@ -0,0 +1,59 @@ +# Releasing + +**A release is started by a person, everything else is automatic.** You decide *when* (and whether it is a patch, +minor or major version); the workflow decides nothing and skips no check. + +## How to release +1. Make sure `main` contains what you want to ship (and is green). +2. GitHub → **Actions → Release → Run workflow** on `main`, or from a terminal: + ``` + gh workflow run release.yml -f bump=minor # patch (default) | minor | major + gh workflow run release.yml -f version=v1.0.0 # an explicit version instead of a bump + gh workflow run release.yml -f dry_run=true # show the version and release notes, publish nothing + gh workflow run release.yml -f draft=true # create a draft release you publish by hand + ``` +3. That's it. The run takes as long as the test suite (~10 minutes). + +## What the workflow does +``` +plan ──► build (build.yml) ─� + └─► tests (tests.yml) ─┴─► release ──► verify +``` +1. **plan** – works out the next version from the latest `vX.Y.Z` git tag (`scripts/releasing/next_version.sh`; it + refuses a version that already exists or is not newer). Only runs from `main`. +2. **build + tests** – the *same* workflows that gate every pull request, run on the exact commit being released. A red + test means no release. The `plugin.wasm` they build is the one that gets published (it is never rebuilt). +3. **release** – writes the notes (the sha256, a ready-to-paste sqlc config, and GitHub's generated list of merged pull + requests, grouped by label as configured in `.github/release.yml`), then creates the tag and the GitHub release with + `sqlc-gen-ruby.wasm` and `sqlc-gen-ruby.wasm.sha256` attached. +4. **verify** – plays the user: downloads the plugin from the release's public URL, checks the sha256 and runs + `sqlc diff` with it over the examples. If this fails, fix forward and release again (a published tag is not rewritten). + +## Versioning +[Semantic versioning](https://semver.org), tags are `vMAJOR.MINOR.PATCH`. The version describes **the generated +code and the options**, because that is what users depend on: + +| Bump | When | +|---|---| +| patch | bug fixes; output changes that cannot break callers (comments, formatting, a fixed type) | +| minor | new options, new query features, new drivers; output that only adds API (e.g. a new method) | +| major | existing generated method signatures, constants or options change incompatibly | + +Label pull requests (`breaking`, `feature`, `bug`, `documentation`; `skip-changelog` to hide one) so the generated notes +are grouped sensibly, and pick the bump from the labels of what is merged since the last release. + +## Using a release +```yaml +plugins: +- name: ruby + wasm: + url: https://github.com/DaredevilOSS/sqlc-gen-ruby/releases/download/v0.1.0/sqlc-gen-ruby.wasm + sha256: +``` + +## If something goes wrong +* *Dry run first when unsure:* `-f dry_run=true` runs the whole gate and prints the notes without publishing. +* *A published release is bad:* mark it as a pre-release or delete the release **and** its tag in the GitHub UI, then fix + and release the next version. Do not re-use a version number: users pin the sha256 of a version. +* *The workflow cannot find a tag / wrong version:* the version comes from `git tag`; check that tags are intact + (`git ls-remote --tags origin`). diff --git a/docs/Testing.md b/docs/Testing.md new file mode 100644 index 0000000..a9551bb --- /dev/null +++ b/docs/Testing.md @@ -0,0 +1,51 @@ +# Testing and CI + +There are four layers. Each one answers a different question, and each runs in CI. + +| Layer | Command | Needs | Question it answers | +|---|---|---|---| +| Unit + golden | `bundle exec rake test` | nothing | does the plugin produce the expected files? | +| Generated-code checks | `bundle exec rake rubocop steep` | nothing | is the generated code clean Ruby with correct types? | +| `sqlc diff` | `rake sqlc_generate_process` / `sqlc_generate_wasm` | sqlc | do the committed `examples/` match the plugin, through the real sqlc and the real WASM? | +| End-to-end | `rake end2end_sqlite3` / `end2end_pg` / `end2end_mysql2` | databases | does the *generated code* behave correctly against a real database? | + +## Unit and golden tests (`test/`) +* `golden_test.rb` runs the plugin on **real requests captured from sqlc** (`test/fixtures/*.bin`) and compares with + the checked-in `examples/`. `extended_golden_test.rb` does the same for option variants (Ruby 3.1/3.2, no types, + `generateGemfile`, ...) and synthetic requests (`synthetic_requests.rb`) against `test/golden/`. + After an intended output change: `UPDATE_GOLDEN=1 bundle exec ruby -Itest test/extended_golden_test.rb`, then **read the diff**. +* `features_test.rb` / `options_test.rb`: one focused test per feature or option. +* `codec_test.rb` cross-checks the hand-written protobuf codec against Google's `google-protobuf` (test-only gem). +* Refreshing a fixture after a new sqlc version or schema change: `scripts/capture_request.rb` (see its header). + +## `examples/` is both documentation and a test +`examples/authors//{schema,query}.sql` are the inputs; `examples//` is the plugin's output, committed. +CI runs `sqlc diff` against it, so a plugin change that is not accompanied by regenerated examples fails the build. +Regenerate with `rake sqlc_generate_process` (pure Ruby, fast) or `rake sqlc_generate_wasm` (builds the WASM first). + +## End-to-end tests (`tests/`) +They `require` the generated code from `examples/` and run it against a real database, with Minitest. +`rbs_types.rb` additionally checks that returned values match the generated `.rbs` types. +* SQLite needs nothing: `rake end2end_sqlite3`. +* PostgreSQL / MySQL: provide a server, load `examples/authors//schema.sql`, and set `DB_NAME DB_USER DB_PASS + POSTGRES_HOST MYSQL_HOST` (optionally `*_PORT`). MySQL needs `local_infile=1` for `:copyfrom`. + Or run everything in Docker: `rake run_end2end_tests` (docker-compose). +* They cover every query command, transactions (nested, isolation), pool injection, logging, bang finders, `copyfrom` + with awkward values, Fiber-scheduler concurrency (`async` gem) and statement-leak checks. + +## CI workflows (`.github/workflows/`) +| Workflow | When | What | +|---|---|---| +| `build.yml` | push to `main`, pull requests (not for Markdown-only changes), release | build `plugin.wasm`; lint; `sqlc diff` with the WASM and the process plugin; Steep; generate with option combinations (Ruby 3.1/3.2/3.3 × types/Gemfile) and check the output with the matching Ruby | +| `tests.yml` | same | unit tests; the plugin on Ruby 3.1 / 3.2 / 3.4; end-to-end against PostgreSQL 16 and MySQL 8.3 service containers and SQLite | +| `docs.yml` | `examples/` change on `main`, manual | regenerates `docs/Examples.md` and opens a PR | +| `release.yml` | manual only | see [Releasing](Releasing.md) | + +Tool versions (sqlc, ruby.wasm) live in `scripts/tool-versions.env`; dependabot updates actions and gems weekly. + +## Before you push +``` +bundle exec rake test rubocop steep +rake sqlc_generate_process && git status --short examples # must show no changes (or commit them) +rake end2end_sqlite3 # plus pg / mysql2 when the generated code changed +``` diff --git a/scripts/releasing/next_version.sh b/scripts/releasing/next_version.sh new file mode 100755 index 0000000..1e7c10b --- /dev/null +++ b/scripts/releasing/next_version.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Prints the version a release would get: next_version.sh [explicit vX.Y.Z] +# The latest `vX.Y.Z` git tag is the current version (v0.0.0 when there is none yet). +set -euo pipefail + +bump=${1:?usage: next_version.sh [vX.Y.Z]} +explicit=${2:-} + +latest=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n 1) +latest=${latest:-v0.0.0} + +if [[ -n "${explicit}" ]]; then + [[ "${explicit}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "'${explicit}' is not of the form vX.Y.Z" >&2; exit 1; } + next=${explicit} +else + IFS=. read -r major minor patch <<< "${latest#v}" + case "${bump}" in + major) next="v$((major + 1)).0.0" ;; + minor) next="v${major}.$((minor + 1)).0" ;; + patch) next="v${major}.${minor}.$((patch + 1))" ;; + *) echo "bump must be patch, minor or major (got '${bump}')" >&2; exit 1 ;; + esac +fi + +if git rev-parse -q --verify "refs/tags/${next}" > /dev/null; then + echo "tag ${next} already exists" >&2; exit 1 +fi +newest=$(printf '%s\n%s\n' "${latest}" "${next}" | sort -V | tail -n 1) +[[ "${newest}" == "${next}" ]] || { echo "${next} is not newer than the latest release ${latest}" >&2; exit 1; } + +echo "latest=${latest}" >&2 +echo "${next}"