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/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..bf67ecb --- /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`) +- [ ] 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 new file mode 100644 index 0000000..7bb809c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,147 @@ +name: Build +defaults: + run: + shell: bash + +permissions: + contents: read + +concurrency: + 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: + 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: grep -E '^[A-Z_]+=' 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: 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 + 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 037deb4..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: CI -defaults: - run: - shell: bash - -permissions: - contents: write - pull-requests: write - -on: - push: - branches: [ "main" ] - paths: - - "**" - - "!**.md" - - "!ruby-wasm-poc/**" - pull_request: - branches: [ "*" ] - paths: - - "**" - - "!**.md" - - "!ruby-wasm-poc/**" - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - - name: Set up .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "8.0.x" - - - name: .NET Lint - uses: zyactions/dotnet-lint@v1 - with: - implicit-restore: true - workspace: sqlc-gen-ruby.sln - exclude: GeneratedProtobuf - - build: - name: Build - runs-on: ${{ matrix.os }} - needs: [lint] - strategy: - matrix: - os: [ 'ubuntu-latest' ] - architecture: [ 'x64', 'arm64' ] - - 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 - 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 - - - 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 - - - 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 - 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.25.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: 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.25.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 ed5b4ee..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/**" - - "Drivers/**" + - "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..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: [CI] - 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@v4 + - 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@v4 - 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: ci.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 new file mode 100644 index 0000000..c1d5f14 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,153 @@ +name: Tests +defaults: + run: + shell: bash + +permissions: + contents: read + +concurrency: + 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: + 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/.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/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/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/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/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/Gemfile b/Gemfile index 526c34b..6bd5784 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,25 @@ 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 'csv' # used by the generated mysql2 code (:copyfrom); a bundled gem since Ruby 3.4 gem 'mysql2' gem 'pg', '1.5.2' gem 'sqlite3' gem 'minitest' gem 'rake' -gem 'steep' \ No newline at end of file +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 + 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..f8c41ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,19 +13,39 @@ 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 + rake (~> 13.3) + google-protobuf (4.36.2-arm64-darwin) + bigdecimal + 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) @@ -34,19 +54,39 @@ 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 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 + 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 + ruby_wasm (2.10.1-arm64-darwin) + logger securerandom (0.3.1) sqlite3 (2.0.2) mini_portile2 (~> 2.8.0) @@ -78,11 +118,16 @@ PLATFORMS ruby DEPENDENCIES + async connection_pool + csv + google-protobuf (~> 4.0) minitest mysql2 pg (= 1.5.2) rake + rubocop + ruby_wasm (~> 2.7) sqlite3 steep 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/README.md b/README.md index 6493586..b2c2f3a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -[![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. +[![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 -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 [How it works](#documentation). ## Usage @@ -14,6 +18,22 @@ Why I didn't use Ruby to generate Ruby in [here](ruby-wasm-poc/README.md). | 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. | +| 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 +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 @@ -64,52 +84,117 @@ 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 +## Using the generated code +```ruby +require_relative 'db/query_sql' -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/) +queries = PgCodegen::QuerySql.connect({ dbname: 'app', host: 'localhost' }, size: 5) # a ConnectionPool -### Protobuf -SQLC protobuf are defined in sqlc-dev/sqlc repository. -Generating Ruby code from protocol buffer files: -``` -rake protobuf_generate -``` +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 -### Generating code -SQLC utilizes our process / WASM plugin to generate code -``` -rake sqlc_generate_process -rake sqlc_generate_wasm +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_:`). +* **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. +* **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 + +### 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` (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 | +|---------------|:---:|:---:|:---:|-------| +| `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. 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` (Ruby has no async/await: use the Fiber scheduler, see above), `useDapper`, target framework / csproj / namespace options (`moduleName` is the equivalent of the namespace). -### Testing generated code -Testing the SQLC generated code via a predefined flow: +# Contributing +## Local plugin development +### Prerequisites +* Ruby 3.3 and `bundle install` +* [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) +* PostgreSQL / MySQL servers for the end-to-end tests (or Docker, see below) + +### 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 | +| `examples/` | the plugin's output for `examples/authors/*` (kept in sync, CI runs `sqlc diff`) | +| `scripts/wasm/` | building the WASM plugin | + +### Everyday commands ``` -rake test_process_plugin -rake test_wasm_plugin +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 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`. + +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`. + +### 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]`. - -* 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 +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). + +## 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/Rakefile b/Rakefile index 96db2b9..c9904d4 100755 --- a/Rakefile +++ b/Rakefile @@ -2,51 +2,62 @@ require 'rake' require 'rake/testtask' -Rake::TestTask.new(:end2end_tests) do |t| # called inside Docker - t.pattern = "tests/end2end_*.rb" +# Plugin unit + golden tests (pure Ruby, no databases needed). +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.pattern = 'test/*_test.rb' end -task :steep do - sh "bundle exec steep check --with-expectations" +Rake::TestTask.new(:end2end_tests) do |t| # called inside Docker + t.pattern = 'tests/end2end_*.rb' end -task :run_end2end_tests do - sh "./scripts/tests/run_end2end.sh" +# 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 :run_tests => [:run_end2end_tests] - -# Other -task :dotnet_format do - sh "dotnet format --exclude GeneratedProtobuf" +task :rubocop do + sh 'bundle exec rubocop examples' end -task :protobuf_generate do - sh "./scripts/generate_protobuf.sh" +task :steep do + sh 'bundle exec steep check --with-expectations' end -task :dotnet_publish_process => [:protobuf_generate, :dotnet_format] do - sh "dotnet publish LocalRunner -c release --output dist/" +task :run_end2end_tests do + sh './scripts/tests/run_end2end.sh' end -task :sqlc_generate_process => [:dotnet_publish_process] do - sh "sqlc -f sqlc.local.yaml generate" +task :run_tests => [:run_end2end_tests] + +# --- Process plugin (exe/plugin.rb run directly by sqlc) --- +task :sqlc_generate_process do + sh 'sqlc -f sqlc.local.yaml generate' end -task :test_process_plugin => [:sqlc_generate_process, :run_tests] +task :test_process_plugin => [:test, :sqlc_generate_process, :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" +# --- 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 :update_wasm_plugin do - sh "./scripts/wasm/update_sha.sh sqlc.ci.yaml" + 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 => [:dotnet_publish_wasm, :update_wasm_plugin] do - ENV['SQLCCACHE'] = './' - sh "sqlc -f sqlc.ci.yaml generate" +task :sqlc_generate_wasm => [:wasm_build, :update_wasm_plugin] do + ENV['SQLCCACHE'] = './' + sh 'sqlc -f sqlc.ci.yaml generate' end -task :test_wasm_plugin => [:sqlc_generate_wasm, :update_wasm_plugin, :run_tests] +task :test_wasm_plugin => [:test, :sqlc_generate_wasm, :update_wasm_plugin, :run_tests] 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/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/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/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/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 new file mode 100644 index 0000000..25373fd --- /dev/null +++ b/docs/Design.md @@ -0,0 +1,58 @@ +# Design: a sqlc plugin written in Ruby, shipped as 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`). + +## Source layout + +| File | Role | +|---|---| +| `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, `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 | + +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 +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 +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. + +## 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 +* 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). +* `3.3.x` (a patch version) is rejected as `rubyVersion`; use `3.3`. 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/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/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index 5c6b9fd..7a60060 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -1,34 +1,90 @@ -/* name: GetAuthor :one */ +-- name: GetAuthor :one +-- Returns the author with the given id, or nil when there is none. 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 +-- 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; + +-- 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 +-- Bulk insert: pass an Array of CopyAuthorsArgs. +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..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; @@ -18,6 +19,85 @@ 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 +-- 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; + +-- 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 +-- Bulk insert: pass an Array of CopyAuthorsArgs. +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..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; @@ -16,3 +17,59 @@ 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 +-- Sets the bio of every author that has one and returns how many were changed. +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 +-- Bulk insert: pass an Array of CopyAuthorsArgs. +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/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 new file mode 100644 index 0000000..3c5c04f --- /dev/null +++ b/examples/mysql2/models.rb @@ -0,0 +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 diff --git a/examples/mysql2/models.rbs b/examples/mysql2/models.rbs new file mode 100644 index 0000000..f1926fc --- /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: 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 aaf9357..2382175 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -1,209 +1,622 @@ -# auto-generated by sqlc - do not edit +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit require 'connection_pool' -require 'mysql2' - +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 - - 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 - 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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['id'], row['name'], row['bio']) + 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 + with_client('list_authors') 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('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 update_author(bio:, id:) + with_client('update_author') 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('create_author_return_id') 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('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 test + with_client('test') 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 + + # 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('list_authors_paged') 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('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['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('update_author_bios') 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('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['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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['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('copy_authors') 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('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:) + with_client('create_book') 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('list_all_authors_books') 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('get_duplicate_authors') 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('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, 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('create_extended_bio') 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('get_first_extended_bio_by_type') 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 + + # 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('truncate_extended_bios') do |client| + stmt = client.prepare(TRUNCATE_EXTENDED_BIOS_SQL) + begin + stmt.execute + nil + ensure + stmt.close + end + 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) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + 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 83d9f88..df71556 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -1,156 +1,266 @@ -# auto-generated by sqlc - do not edit +# 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 - - 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? - 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 + class NotFound < StandardError + end + + 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 + + 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 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 get_first_extended_bio_by_type!: (bio_type: String?) -> GetFirstExtendedBioByTypeRow + + def truncate_extended_bios: -> void + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..6d0eee8 --- /dev/null +++ b/examples/pg/models.rb @@ -0,0 +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 diff --git a/examples/pg/models.rbs b/examples/pg/models.rbs new file mode 100644 index 0000000..b97a45b --- /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: 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 d8c7630..9849d78 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -1,165 +1,631 @@ -# auto-generated by sqlc - do not edit +# 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 - - 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 - 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 + + # 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('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') + 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('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) + 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 + + # 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('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) + 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('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') + 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 + + # 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + 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') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + 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) + 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('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') + 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('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') + 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('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) + 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('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) + 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('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) + 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 + + # 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('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') + 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('insert_extra_types') 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('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') + 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 + + # 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('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') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + 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) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + 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 87aefaf..7c71e61 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -1,106 +1,275 @@ -# auto-generated by sqlc - do not edit +# 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 - - 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? - 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 + class NotFound < StandardError + end + + 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] + + 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 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 instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (untyped) -> T } -> T + + def configure: (untyped client) -> void + end +end diff --git a/examples/sqlite3/models.rb b/examples/sqlite3/models.rb new file mode 100644 index 0000000..bf6966f --- /dev/null +++ b/examples/sqlite3/models.rb @@ -0,0 +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 diff --git a/examples/sqlite3/models.rbs b/examples/sqlite3/models.rbs new file mode 100644 index 0000000..cd9946a --- /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 diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index 6bca88f..109053c 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -1,77 +1,412 @@ -# auto-generated by sqlc - do not edit +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit require 'connection_pool' -require 'sqlite3' - +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 - - 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 - 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 + # 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) + 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:) + 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 + 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 + end + end + + def create_author(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_author_by_name_pattern(name_pattern: nil) + 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 + end + end + + def create_author_return_id(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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 + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + 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 + end + + def truncate_authors + 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:) + 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 + 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 + end + end + + def get_duplicate_authors + 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 + end + end + + def get_authors_by_book_name(name:) + 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 + 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 + end +end diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index 5b09491..96a1a35 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -1,53 +1,161 @@ -# auto-generated by sqlc - do not edit +# 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 - - 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 - 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 + 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 + + 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 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/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/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 new file mode 100644 index 0000000..15a1b01 --- /dev/null +++ b/lib/sqlc_gen_ruby/code.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module SqlcGenRuby + # Small helpers for building indented Ruby / RBS source text. + module Code + MAX_ELEMENTS_PER_LINE = 5 + + module_function + + INDENT = ' ' + + # 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) + 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) + raise ArgumentError, 'input cannot be empty' if str.empty? + + 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 + + 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 + end +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..58ea322 --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require_relative '../code' + +module SqlcGenRuby + module Drivers + # 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. + # 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) + + # 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, _ruby_version) = [] + + # Extra gems for the generated Gemfile (e.g. `csv`, a bundled gem since Ruby 3.4). + def extra_gems(_queries) = [] + + # Ruby source (and its RBS) placed in the module next to the query classes: shared helpers / constants. + # [[code, rbs], ...] + def module_members = [] + + # Public methods of the generated query class that are not queries: constructor, `connect`, `transaction`. + # [[code, rbs], ...] + def class_methods = raise(NotImplementedError) + + # Private helpers of the query class. [[code, rbs], ...] + def private_methods = [] + + # How a query method acquires the database connection (yielding `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 + + # 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(_stmt_name, _const, _has_params, array_rows: false) = raise(NotImplementedError) + + # 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 + + # 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) + + # 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) + + # 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. + # 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) } + else + mapping.db_types.include?(db_type) + end + return mapping.ruby_type if matched + end + return fallback unless 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.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 + + # 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 | ' \ + ':serializable)?) { (instance) -> T } -> T' + + 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) + + 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 new file mode 100644 index 0000000..eaac2a4 --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require_relative 'base' + +module SqlcGenRuby + module Drivers + # MySQL / MariaDB via the `mysql2` gem (prepared statements, closed after use). + class Mysql2 < Base + def name = 'Mysql2' + + def module_members + [ + [CONNECTION_STATE, "module ConnectionState\n attr_accessor in_transaction: bool\nend"], + [ISOLATION_LEVELS, ISOLATION_RBS] + ] + end + + def class_methods + [ + [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 + [[INSTRUMENT, INSTRUMENT_RBS], [WITH_CLIENT, '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) + 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 exec_rows_lines(exec) = [exec, 'stmt.affected_rows'] + + 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(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 #{rows_var}.empty?", + '', + Code.block("Tempfile.create(['#{label}', '.csv']) do |file|", "#{write}\nfile.flush\n#{load_data}"), + 'nil' + ] + end + + 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, '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 + + 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, logger: Logger.new($stdout)) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(mysql2_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + 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('transaction') 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(name) + instrument(name) do + @db.with do |client| + client.extend(ConnectionState) unless client.is_a?(ConnectionState) + yield client + end + 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 = <<~'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 + + 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('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('BigDecimal', %w[decimal numeric dec fixed]), + mapping('Date', %w[date]), + mapping('Time', %w[datetime timestamp time]) + ] + end + end + end +end diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb new file mode 100644 index 0000000..954cad9 --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require_relative 'base' + +module SqlcGenRuby + module Drivers + # 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 module_members + [ + [CONNECTION_STATE, "module ConnectionState\n def prepared_statements: () -> Set[String]\nend"], + [ISOLATION_LEVELS, ISOLATION_RBS] + ] + end + + def class_methods + [ + [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 + [ + [INSTRUMENT, INSTRUMENT_RBS], + [WITH_CLIENT, 'def with_client: [T] (String name) { (untyped) -> T } -> T'], + [CONFIGURE, 'def configure: (untyped client) -> void'] + ] + end + + # 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 + + # 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}", '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}", "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(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), + '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 + + 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, logger: Logger.new($stdout)) + # (pool_options go to ConnectionPool.new). To share an existing pool use QuerySql.new(pool). + def self.connect(pg_params, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + 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('transaction') 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(name) + instrument(name) do + @db.with do |client| + configure(client) unless client.is_a?(ConnectionState) + yield client + end + 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] + + 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('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[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[json jsonb]) + ] + 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..0c34686 --- /dev/null +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require_relative 'base' + +module SqlcGenRuby + module Drivers + # 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 class_methods + [ + [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 row_data_type = :array + + def closes_statements? = true + + def supports_slice? = true + + def exec_rows_lines(exec) = [exec, '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', 'row && row[0]'] + end + + # One multi-row `INSERT ... VALUES (?, ?), (?, ?), ...` per batch. + 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 { |row| [#{attrs.map { |a| "row.#{a}" }.join(', ')}] })" + ].join("\n") + [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) + 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 + + private + + 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 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) + ] + end + end + end +end diff --git a/lib/sqlc_gen_ruby/generator.rb b/lib/sqlc_gen_ruby/generator.rb new file mode 100644 index 0000000..19039c8 --- /dev/null +++ b/lib/sqlc_gen_ruby/generator.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +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, extended with models, enums, sqlc.embed, ... + class Generator + 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) + @driver = instantiate_driver + @catalog = Catalog.new(request.catalog) + @driver.catalog = @catalog + @types = TypeResolver.new(@driver, @options) + @layout = RowLayout.new(@driver, @catalog, @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?, with_connection: !connection.empty?) + end + files.concat(models).concat(connection) + 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 + 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 = @options.module_name || "#{@driver.name}Codegen" + + # 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 + + 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, 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\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, 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 (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), "#{Code.sql_constant(query.name)}: String"] + end + members << dataclass("#{Code.upcase_first(query.name)}Row", row_pairs(query)) unless query.columns.empty? + 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] } + + # 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")}", "private\n\n#{helpers.map(&:last).join("\n\n")}"] + end + members + end + + # --- 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? + + @options.ruby_version.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 { |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}" })) + init = Code.indent("def self.new: (#{init_args}) -> #{name}") + [code, "class #{name} < ::Struct\n#{accessors}\n#{init}\nend"] + end + + # --- 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 = "#{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 = "#{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} = #{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| + 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 +end diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb new file mode 100644 index 0000000..f4691fd --- /dev/null +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require_relative 'code' + +module SqlcGenRuby + # 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 values.empty? ? 'NULL' : Array.new(values.size, '?').join(', ')\nend", + 'def slice_placeholders: (Array[untyped] values) -> String' + ].freeze + + # 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) + 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(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) + func = Code.snake_case(query.name) + 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 + 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. + 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 ':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, 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('rows', name, params.map { |p| p.column.name }, params.map(&:name), + Code.snake_case(query.name)) + end + + 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(params) unless params.empty? + const = Code.sql_constant(query.name) + if slice?(query) + statements << slice_sql(const, params) + const = 'sql' + end + 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 stmt.close\nend") + else + statements.concat(commands) + end + statements + end + + def query_params(params) + list = params.map { |p| p.slice ? "*#{p.name}" : p.name } + "query_params = [#{Code.comma_join(list)}]" + end + + # 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 + + # 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', "row && #{new_row(query)}"] + when ':many' + ["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 + + def method_body(func, statements) + Code.block(@driver.client_block(func), statements.join("\n")) + end + + def new_row(query) + 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 new file mode 100644 index 0000000..4f20013 --- /dev/null +++ b/lib/sqlc_gen_ruby/options.rb @@ -0,0 +1,87 @@ +# 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 + + # `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, :module_name, + :prepared_statements + + 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')) + @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". + 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) + + value = raw.fetch(key, default) + raise ArgumentError, "option #{key} must be a string" unless value.is_a?(String) + + 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) + + value + end + end +end diff --git a/lib/sqlc_gen_ruby/plugin_pb.rb b/lib/sqlc_gen_ruby/plugin_pb.rb new file mode 100644 index 0000000..1745d1a --- /dev/null +++ b/lib/sqlc_gen_ruby/plugin_pb.rb @@ -0,0 +1,97 @@ +# 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 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) 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 + field :global_options, 6, :bytes + end + + class GenerateResponse < P + field :files, 1, :message, repeated: true, klass: File + end + end +end diff --git a/lib/sqlc_gen_ruby/proto.rb b/lib/sqlc_gen_ruby/proto.rb new file mode 100644 index 0000000..afa60e5 --- /dev/null +++ b/lib/sqlc_gen_ruby/proto.rb @@ -0,0 +1,202 @@ +# 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 + + attr_reader :pos + + 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/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/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/lib/sqlc_gen_ruby/type_resolver.rb b/lib/sqlc_gen_ruby/type_resolver.rb new file mode 100644 index 0000000..9669777 --- /dev/null +++ b/lib/sqlc_gen_ruby/type_resolver.rb @@ -0,0 +1,36 @@ +# 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 + + 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) + "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/ruby-wasm-poc/Gemfile b/ruby-wasm-poc/Gemfile deleted file mode 100644 index 29d26f4..0000000 --- a/ruby-wasm-poc/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source 'https://rubygems.org' - -gem "ruby_wasm", "~> 2.6" -gem "ruby-next", "~> 1.0" -gem "google-protobuf", "~> 3.25" -gem "grpc", "~> 1.63" 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 deleted file mode 100644 index cdc6cc5..0000000 --- a/ruby-wasm-poc/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# 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. -``` - -## 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. 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/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/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/scripts/capture_request.rb b/scripts/capture_request.rb new file mode 100755 index 0000000..a82bbce --- /dev/null +++ b/scripts/capture_request.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# A sqlc *process plugin* that records the GenerateRequest sqlc sends it, to refresh test/fixtures/*.bin. +# 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 +# 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/catalog' +require 'sqlc_gen_ruby/plugin_pb' + +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/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/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/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}" 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 new file mode 100755 index 0000000..bca558b --- /dev/null +++ b/scripts/wasm/build.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 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")/../.." +# 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 + 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 + +# 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}/${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" +# 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 +# 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 +$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 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/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/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; } 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 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" diff --git a/steep_expectations.yml b/steep_expectations.yml index 4e2dbab..cb894d1 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,53 +3,103 @@ diagnostics: - range: start: - line: 98 - character: 9 + line: 198 + character: 10 end: - line: 98 - character: 23 + line: 198 + character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 99 - character: 4 + line: 198 + character: 47 end: - line: 99 - character: 10 + line: 198 + character: 53 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant + - range: + start: + line: 615 + character: 6 + end: + line: 615 + character: 9 + severity: WARNING + message: 'Cannot find the declaration of constant: `CSV`' + code: Ruby::UnknownConstant - file: examples/pg/query_sql.rb diagnostics: - range: start: - line: 68 - character: 9 + line: 213 + character: 10 end: - line: 68 - character: 23 + line: 213 + character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 69 - character: 13 + line: 213 + character: 47 + end: + line: 213 + character: 49 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 227 + character: 49 + end: + line: 227 + character: 51 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 427 + character: 18 + end: + line: 427 + character: 20 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 624 + character: 17 end: - line: 69 - character: 15 + line: 624 + character: 19 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 70 + line: 625 character: 34 end: - line: 70 + line: 625 character: 36 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant + - range: + start: + line: 627 + character: 36 + end: + line: 627 + 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 new file mode 100644 index 0000000..d84ea48 --- /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 + + # 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 + 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_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 + 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 new file mode 100644 index 0000000..c8ab31e --- /dev/null +++ b/test/extended_golden_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'test_helper' +require_relative 'synthetic_requests' + +# 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 + + %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..c6753f0 --- /dev/null +++ b/test/features_test.rb @@ -0,0 +1,242 @@ +# 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, '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'), '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'), '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 = 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 + request = load_request('pg') + query(request, 'GetAuthorsByIds').params.first.column.is_sqlc_slice = true + assert_raises(RuntimeError) { generate(request) } + end + + def test_command_signatures + rbs = generate(load_request('pg')).fetch('query_sql.rbs') + 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 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 + %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\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 + skip 'Data needs Ruby 3.2' unless defined?(Data) + 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 + + # ---- 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/fixtures/mysql2.bin b/test/fixtures/mysql2.bin new file mode 100644 index 0000000..eef840c --- /dev/null +++ b/test/fixtures/mysql2.bin @@ -0,0 +1,235 @@ +”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 + +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", +name0ÿÿÿÿÿÿÿÿÿR authorsbtextzname"( +bio0ÿÿÿÿÿÿÿÿÿR authorsbtextzbio*.* +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", +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 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ÿÿÿÿÿÿÿÿÿ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"( +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ÿÿÿÿÿÿÿÿÿ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 +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/pg.bin b/test/fixtures/pg.bin new file mode 100644 index 0000000..75b705d Binary files /dev/null and b/test/fixtures/pg.bin differ diff --git a/test/fixtures/sqlite3.bin b/test/fixtures/sqlite3.bin new file mode 100644 index 0000000..c41d644 Binary files /dev/null and b/test/fixtures/sqlite3.bin differ diff --git a/test/golden/mysql2/default/connection.rb b/test/golden/mysql2/default/connection.rb new file mode 100644 index 0000000..9c5d70e --- /dev/null +++ b/test/golden/mysql2/default/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/default/connection.rbs b/test/golden/mysql2/default/connection.rbs new file mode 100644 index 0000000..4af350d --- /dev/null +++ b/test/golden/mysql2/default/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/default/models.rb b/test/golden/mysql2/default/models.rb new file mode 100644 index 0000000..3c5c04f --- /dev/null +++ b/test/golden/mysql2/default/models.rb @@ -0,0 +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 diff --git a/test/golden/mysql2/default/models.rbs b/test/golden/mysql2/default/models.rbs new file mode 100644 index 0000000..f1926fc --- /dev/null +++ b/test/golden/mysql2/default/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: 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 new file mode 100644 index 0000000..05bd12b --- /dev/null +++ b/test/golden/mysql2/default/my_queries_sql.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + 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 +end diff --git a/test/golden/mysql2/default/my_queries_sql.rbs b/test/golden/mysql2/default/my_queries_sql.rbs new file mode 100644 index 0000000..78fae5b --- /dev/null +++ b/test/golden/mysql2/default/my_queries_sql.rbs @@ -0,0 +1,23 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + class NotFound < StandardError + end + + 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 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/mysql2/default/other_sql.rb b/test/golden/mysql2/default/other_sql.rb new file mode 100644 index 0000000..f204cec --- /dev/null +++ b/test/golden/mysql2/default/other_sql.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + with_client('other_one') 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 + + # 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('count_rows') 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 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 +end diff --git a/test/golden/mysql2/default/other_sql.rbs b/test/golden/mysql2/default/other_sql.rbs new file mode 100644 index 0000000..572546b --- /dev/null +++ b/test/golden/mysql2/default/other_sql.rbs @@ -0,0 +1,38 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + 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 + class NotFound < StandardError + end + + 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 no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + 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/mysql2/default/query_sql.rb b/test/golden/mysql2/default/query_sql.rb new file mode 100644 index 0000000..a4cfd49 --- /dev/null +++ b/test/golden/mysql2/default/query_sql.rb @@ -0,0 +1,712 @@ +# 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 + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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['id'], row['name'], row['bio']) + 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 + with_client('list_authors') 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('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 update_author(bio:, id:) + with_client('update_author') 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('create_author_return_id') 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('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 test + with_client('test') 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 + + # 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('list_authors_paged') 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('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['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('update_author_bios') 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('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['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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['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('copy_authors') 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('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:) + with_client('create_book') 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('list_all_authors_books') 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('get_duplicate_authors') 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('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, 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('create_extended_bio') 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('get_first_extended_bio_by_type') 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 + + # 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('truncate_extended_bios') 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('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['c0'], row['c1']) + 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:) + 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['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client('four_cols') 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 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 + + # 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 new file mode 100644 index 0000000..a44461a --- /dev/null +++ b/test/golden/mysql2/default/query_sql.rbs @@ -0,0 +1,309 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + 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 + class NotFound < StandardError + end + + 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 + + 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 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 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 instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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/Gemfile b/test/golden/mysql2/ruby31_gemfile/Gemfile new file mode 100644 index 0000000..2fe5953 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'connection_pool' +gem 'mysql2' +gem 'csv' \ No newline at end of file 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 new file mode 100644 index 0000000..7a5a36d --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/models.rb @@ -0,0 +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 diff --git a/test/golden/mysql2/ruby31_gemfile/models.rbs b/test/golden/mysql2/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..a728153 --- /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: 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 new file mode 100644 index 0000000..05bd12b --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + 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 +end diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs new file mode 100644 index 0000000..78fae5b --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,23 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + class NotFound < StandardError + end + + 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 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/mysql2/ruby31_gemfile/other_sql.rb b/test/golden/mysql2/ruby31_gemfile/other_sql.rb new file mode 100644 index 0000000..8595621 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + with_client('other_one') 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 + + # 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('count_rows') 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 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 +end diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rbs b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs new file mode 100644 index 0000000..b31697b --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,38 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + 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 + class NotFound < StandardError + end + + 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 no_cols: (c0: Integer, c1: String, c2: Float, c3: String, c4: Integer) -> void + + 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/mysql2/ruby31_gemfile/query_sql.rb b/test/golden/mysql2/ruby31_gemfile/query_sql.rb new file mode 100644 index 0000000..833d0a6 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rb @@ -0,0 +1,712 @@ +# 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 + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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['id'], row['name'], row['bio']) + 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 + with_client('list_authors') 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('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 update_author(bio:, id:) + with_client('update_author') 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('create_author_return_id') 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('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 test + with_client('test') 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 + + # 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('list_authors_paged') 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('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['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('update_author_bios') 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('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['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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['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('copy_authors') 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('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:) + with_client('create_book') 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('list_all_authors_books') 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('get_duplicate_authors') 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('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, 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('create_extended_bio') 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('get_first_extended_bio_by_type') 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 + + # 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('truncate_extended_bios') 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('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['c0'], row['c1']) + 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:) + 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['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client('four_cols') 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 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 + + # 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 new file mode 100644 index 0000000..848d0a2 --- /dev/null +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,309 @@ +# auto-generated by sqlc - do not edit +module Mysql2Codegen + 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 + class NotFound < StandardError + end + + 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 + + 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 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 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 instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..3c5c04f --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/models.rb @@ -0,0 +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 diff --git a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..05bd12b --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + 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 +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..f204cec --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/other_sql.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'mysql2' +require_relative 'connection' +require_relative 'models' + +module Mysql2Codegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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 + with_client('other_one') 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 + + # 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('count_rows') 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 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 +end 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..a4cfd49 --- /dev/null +++ b/test/golden/mysql2/ruby32_notypes/query_sql.rb @@ -0,0 +1,712 @@ +# 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 + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { Mysql2::Client.new(**mysql2_params) }, logger: logger) + end + + # 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('transaction') 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('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['id'], row['name'], row['bio']) + 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 + with_client('list_authors') 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('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 update_author(bio:, id:) + with_client('update_author') 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('create_author_return_id') 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('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 test + with_client('test') 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 + + # 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('list_authors_paged') 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('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['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('update_author_bios') 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('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['id'], row['name'], row['bio']) + end + ensure + stmt.close + end + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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['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('copy_authors') 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('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:) + with_client('create_book') 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('list_all_authors_books') 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('get_duplicate_authors') 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('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, 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('create_extended_bio') 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('get_first_extended_bio_by_type') 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 + + # 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('truncate_extended_bios') 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('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['c0'], row['c1']) + 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:) + 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['c0'], + row['c1'], + row['c2'], + row['c3'], + row['c4'] + ) + end + ensure + stmt.close + end + end + end + + def four_cols + with_client('four_cols') 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 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 + + # 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 new file mode 100644 index 0000000..6d0eee8 --- /dev/null +++ b/test/golden/pg/default/models.rb @@ -0,0 +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 diff --git a/test/golden/pg/default/models.rbs b/test/golden/pg/default/models.rbs new file mode 100644 index 0000000..b97a45b --- /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: 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 new file mode 100644 index 0000000..813f87e --- /dev/null +++ b/test/golden/pg/default/my_queries_sql.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..c31b4fb --- /dev/null +++ b/test/golden/pg/default/my_queries_sql.rbs @@ -0,0 +1,25 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + class NotFound < StandardError + end + + 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 with_numbers2_go: (c0: bool) -> void + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..91b8418 --- /dev/null +++ b/test/golden/pg/default/other_sql.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('no_cols') 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('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') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..e3a1a3f --- /dev/null +++ b/test/golden/pg/default/other_sql.rbs @@ -0,0 +1,40 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + 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 + class NotFound < StandardError + end + + 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 no_cols: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> void + + def other_one: -> OtherOneRow? + + def other_one!: -> OtherOneRow + + def count_rows: (c0: bool) -> Integer + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..3221290 --- /dev/null +++ b/test/golden/pg/default/query_sql.rb @@ -0,0 +1,718 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 + + # 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('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') + 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('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) + 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 + + # 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('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) + 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('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') + 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 + + # 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + 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') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + 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) + 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('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') + 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('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') + 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('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) + 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('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) + 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('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) + 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 + + # 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('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') + 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('insert_extra_types') 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('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') + 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 + + # 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('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') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client('many_params') 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 + + # 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('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) + 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('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') + 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 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 + + # 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 new file mode 100644 index 0000000..28acc6c --- /dev/null +++ b/test/golden/pg/default/query_sql.rbs @@ -0,0 +1,318 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + 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 + class NotFound < StandardError + end + + 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] + + 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 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 instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..6b58541 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'connection_pool' +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 new file mode 100644 index 0000000..e3d3438 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/models.rb @@ -0,0 +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 diff --git a/test/golden/pg/ruby31_gemfile/models.rbs b/test/golden/pg/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..9d9bf3c --- /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: 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 new file mode 100644 index 0000000..1982f7c --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,99 @@ +# 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 + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..c31b4fb --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,25 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + class NotFound < StandardError + end + + 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 with_numbers2_go: (c0: bool) -> void + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..fc9b664 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -0,0 +1,147 @@ +# 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 + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('no_cols') 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('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') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..e422645 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,40 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + 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 + class NotFound < StandardError + end + + 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 no_cols: (c0: bool, c1: Integer, c2: String, c3: Float, c4: String) -> void + + def other_one: -> OtherOneRow? + + def other_one!: -> OtherOneRow + + def count_rows: (c0: bool) -> Integer + + private + + def instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..1f47341 --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -0,0 +1,719 @@ +# 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 + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 + + # 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('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') + 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('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) + 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 + + # 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('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) + 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('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') + 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 + + # 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + 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') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + 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) + 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('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') + 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('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') + 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('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) + 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('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) + 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('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) + 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 + + # 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('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') + 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('insert_extra_types') 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('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') + 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 + + # 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('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') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client('many_params') 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 + + # 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('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) + 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('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') + 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 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 + + # 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 new file mode 100644 index 0000000..e849f4e --- /dev/null +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,318 @@ +# auto-generated by sqlc - do not edit +module PgCodegen + 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 + class NotFound < StandardError + end + + 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] + + 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 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 instrument: [T] (String name) { () -> T } -> T + + def with_client: [T] (String name) { (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 new file mode 100644 index 0000000..6d0eee8 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/models.rb @@ -0,0 +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 diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..813f87e --- /dev/null +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..91b8418 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('no_cols') 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('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') + end + result = client.exec_prepared('other_sql_other_one') + row = result.first + row && OtherOneRow.new(row['c0']) + 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('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) + 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 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 + + # 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 new file mode 100644 index 0000000..3221290 --- /dev/null +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -0,0 +1,718 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'pg' +require_relative 'connection' +require_relative 'models' + +module PgCodegen + 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 + # 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, logger: nil, **pool_options) + new(ConnectionPool.new(**pool_options) { PG.connect(**pg_params) }, logger: logger) + end + + # 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('transaction') 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('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) + 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 + + # 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('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') + 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('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) + 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 + + # 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('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) + 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('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') + 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 + + # 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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) + 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('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| + client.put_copy_data([row.name, row.bio]) + end + end + nil + end + end + + def truncate_authors + 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') + end + client.exec_prepared('query_sql_truncate_authors') + nil + end + end + + def create_book(name:, author_id:) + 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) + 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('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') + 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('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') + 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('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) + 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('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) + 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('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) + 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 + + # 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('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') + 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('insert_extra_types') 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('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') + 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 + + # 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('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') + end + client.exec_prepared('query_sql_truncate_extra_types') + nil + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + with_client('many_params') 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 + + # 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('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) + 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('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') + 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 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 + + # 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 new file mode 100644 index 0000000..bf6966f --- /dev/null +++ b/test/golden/sqlite3/default/models.rb @@ -0,0 +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 diff --git a/test/golden/sqlite3/default/models.rbs b/test/golden/sqlite3/default/models.rbs new file mode 100644 index 0000000..cd9946a --- /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 diff --git a/test/golden/sqlite3/default/my_queries_sql.rb b/test/golden/sqlite3/default/my_queries_sql.rb new file mode 100644 index 0000000..11eaa39 --- /dev/null +++ b/test/golden/sqlite3/default/my_queries_sql.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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) + 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:) + 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 + 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 + end +end diff --git a/test/golden/sqlite3/default/my_queries_sql.rbs b/test/golden/sqlite3/default/my_queries_sql.rbs new file mode 100644 index 0000000..290d346 --- /dev/null +++ b/test/golden/sqlite3/default/my_queries_sql.rbs @@ -0,0 +1,21 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + 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 new file mode 100644 index 0000000..677ee1b --- /dev/null +++ b/test/golden/sqlite3/default/other_sql.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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:) + 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 + 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 + end +end diff --git a/test/golden/sqlite3/default/other_sql.rbs b/test/golden/sqlite3/default/other_sql.rbs new file mode 100644 index 0000000..6d993a0 --- /dev/null +++ b/test/golden/sqlite3/default/other_sql.rbs @@ -0,0 +1,36 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + 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 + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> 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 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 new file mode 100644 index 0000000..a6c17be --- /dev/null +++ b/test/golden/sqlite3/default/query_sql.rb @@ -0,0 +1,502 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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 + end + end + + def create_author(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_author_by_name_pattern(name_pattern: nil) + 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 + end + end + + def create_author_return_id(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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 + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + 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 + end + + def truncate_authors + 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:) + 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 + 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 + end + end + + def get_duplicate_authors + 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 + end + end + + def get_authors_by_book_name(name:) + 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 + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + 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:) + 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 + end + end + + def four_cols + 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 + 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 + end +end diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs new file mode 100644 index 0000000..d4bd9f5 --- /dev/null +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -0,0 +1,204 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + 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 + 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 + + 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_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/Gemfile b/test/golden/sqlite3/ruby31_gemfile/Gemfile new file mode 100644 index 0000000..ea36ae7 --- /dev/null +++ b/test/golden/sqlite3/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/ruby31_gemfile/models.rb b/test/golden/sqlite3/ruby31_gemfile/models.rb new file mode 100644 index 0000000..f5eca1d --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/models.rb @@ -0,0 +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 diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rbs b/test/golden/sqlite3/ruby31_gemfile/models.rbs new file mode 100644 index 0000000..857a90e --- /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 diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb new file mode 100644 index 0000000..11eaa39 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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) + 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:) + 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 + 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 + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs new file mode 100644 index 0000000..290d346 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/my_queries_sql.rbs @@ -0,0 +1,21 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + WITH_NUMBERS2_GO_SQL: String + + class My_queriesSql + 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 new file mode 100644 index 0000000..dc840cd --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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:) + 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 + 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 + end +end diff --git a/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs new file mode 100644 index 0000000..2791fe8 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/other_sql.rbs @@ -0,0 +1,36 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + 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 + class NotFound < StandardError + end + + def initialize: (untyped db, ?logger: untyped) -> 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 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 new file mode 100644 index 0000000..33e66f8 --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -0,0 +1,502 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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 + end + end + + def create_author(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_author_by_name_pattern(name_pattern: nil) + 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 + end + end + + def create_author_return_id(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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 + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + 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 + end + + def truncate_authors + 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:) + 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 + 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 + end + end + + def get_duplicate_authors + 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 + end + end + + def get_authors_by_book_name(name:) + 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 + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + 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:) + 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 + end + end + + def four_cols + 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 + 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 + end +end 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..460d54e --- /dev/null +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -0,0 +1,204 @@ +# auto-generated by sqlc - do not edit +module Sqlite3Codegen + 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 + 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 + + 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_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/models.rb b/test/golden/sqlite3/ruby32_notypes/models.rb new file mode 100644 index 0000000..bf6966f --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/models.rb @@ -0,0 +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 diff --git a/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb new file mode 100644 index 0000000..11eaa39 --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/my_queries_sql.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + WITH_NUMBERS2_GO_SQL = <<~'SQL' + SELECT 1 -- ? ? + WHERE a = ? + SQL + + class My_queriesSql + # 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) + 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:) + 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 + 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 + end +end diff --git a/test/golden/sqlite3/ruby32_notypes/other_sql.rb b/test/golden/sqlite3/ruby32_notypes/other_sql.rb new file mode 100644 index 0000000..677ee1b --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/other_sql.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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:) + 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 + 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 + end +end 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..a6c17be --- /dev/null +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -0,0 +1,502 @@ +# frozen_string_literal: true + +# auto-generated by sqlc - do not edit +require 'connection_pool' +require 'sqlite3' +require_relative 'models' + +module Sqlite3Codegen + 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 + # 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) + 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:) + 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 + 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 + end + end + + def create_author(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_author_by_name_pattern(name_pattern: nil) + 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 + end + end + + def create_author_return_id(name:, bio:) + 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:) + 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:) + 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 + end + end + + def get_authors_by_ids_and_names(ids:, names:) + 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 + end + end + + # Bulk insert: pass an Array of CopyAuthorsArgs. + def copy_authors(rows) + 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 + end + + def truncate_authors + 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:) + 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 + 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 + end + end + + def get_duplicate_authors + 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 + end + end + + def get_authors_by_book_name(name:) + 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 + end + end + + def many_params(c0:, c1:, c2:, c3:, c4:) + 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:) + 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 + end + end + + def four_cols + 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 + 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 + end +end diff --git a/test/golden_test.rb b/test/golden_test.rb new file mode 100644 index 0000000..a95b2ea --- /dev/null +++ b/test/golden_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +# 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 + + 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 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" + end + end + end +end diff --git a/test/official_protobuf.rb b/test/official_protobuf.rb new file mode 100644 index 0000000..46732d5 --- /dev/null +++ b/test/official_protobuf.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# 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 + 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/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/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 new file mode 100644 index 0000000..6ae86b1 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'open3' +require 'json' +require 'fileutils' +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 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..8f038cd 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -1,69 +1,349 @@ #!/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' +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. 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) + include Mysql2Codegen + + 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.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(name: name, bio: bio) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) + @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(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(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(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(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(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(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(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(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(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 + + # ---- sqlc.slice ------------------------------------------------------------------------------------------------ + + def test_slice + ids = %w[a b c].map { |name| create_author(name) } + 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(ids: [ids.last]).map(&:id) + end + + def test_empty_slice_matches_nothing + create_author('a') + 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(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(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(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 } + 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(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 + + 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(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 + end + + # ---- transactions ---------------------------------------------------------------------------------------------- + + def test_transaction_commits_and_returns_the_block_value + result = @query_sql.transaction do |tx| + 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 + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end - 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_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'rolled back', bio: nil) + raise 'boom' + end 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 + assert_empty @query_sql.list_authors + end + + def test_transaction_is_isolated_until_commit + other = QuerySql.connect(mysql2_params) + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'uncommitted', bio: nil) + assert_empty other.list_authors end + 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 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_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 + + 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 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_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 -end \ No newline at end of file + 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 + + # ---- 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 + 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..574c789 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -1,70 +1,420 @@ #!/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' +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. 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) + include PgCodegen + + 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.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(name: name, bio: bio) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + 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(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(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(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(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(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(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(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(name_pattern: nil) + assert_equal 3, all.size + ab = @query_sql.get_author_by_name_pattern(name_pattern: 'ab%') + assert_equal %w[abc abd], ab.map(&:name).sort + end + + # ---- array parameters (PostgreSQL has no sqlc.slice) ----------------------------------------------------------- + + 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(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 + + # ---- :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(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 } + 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(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 + + 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(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(author_name: 'a', name: 'n', bio_type: 'Fiction') end + end - def test_flow - first_inserted_id = create_first_author_and_assert - create_second_author_and_assert - delete_author_and_assert(first_inserted_id) + # ---- transactions ---------------------------------------------------------------------------------------------- + + def test_transaction_commits_and_returns_the_block_value + result = @query_sql.transaction do |tx| + 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 + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end + + def test_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'rolled back', bio: nil) + raise 'boom' + end end + assert_empty @query_sql.list_authors + 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 + def test_transaction_is_isolated_until_commit + other = QuerySql.connect(pg_params) + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'uncommitted', bio: nil) + assert_empty other.list_authors end + assert_equal ['uncommitted'], other.list_authors.map(&:name) + end + + # ---- connection pool ------------------------------------------------------------------------------------------- - 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_every_pooled_connection_prepares_its_own_statements + pooled = QuerySql.connect(pg_params, size: 3) + 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 + uuid = '0e52082f-bf69-480e-aff0-1a85139a1074' + time = Time.utc(2024, 5, 6, 7, 8, 9) + @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) + 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( + 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 - 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 raw_client + PG.connect(**pg_params.transform_keys(&:to_sym)) + 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( + 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 -end \ No newline at end of file + 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 + + # ---- 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 + 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..5096ca1 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -1,71 +1,246 @@ #!/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' 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 + + 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(name: name, bio: bio) + end + + # ---- :one / :many / :exec ------------------------------------------------------------------------------------- + + def test_flow + first = create_author(Consts::BOJACK_AUTHOR, Consts::BOJACK_THEME) + @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(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(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(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(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(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(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(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 + + # ---- sqlc.slice ------------------------------------------------------------------------------------------------ + + def test_slice + ids = %w[a b c].map { |name| create_author(name) } + 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(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(ids: ids, names: %w[b c x]) + assert_equal %w[b c], got.map(&:name).sort + end + + # ---- :copyfrom ------------------------------------------------------------------------------------------------- - 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_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(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 } + 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(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 + + 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(name: 'in tx', bio: nil) + tx.create_book(name: 'book', author_id: id) + :done 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 + assert_equal :done, result + assert_equal ['in tx'], @query_sql.list_authors.map(&:name) + end + + def test_transaction_rolls_back_on_error + assert_raises(RuntimeError) do + @query_sql.transaction do |tx| + tx.create_author_return_id(name: 'rolled back', bio: nil) + raise 'boom' + end end + 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 - 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) + 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 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_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 -end \ No newline at end of file + 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 + + # ---- 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 + assert_equal %i[id name bio], Author.members + assert_equal %i[id name author_id description], Book.members + end +end diff --git a/tests/rbs_types.rb b/tests/rbs_types.rb new file mode 100644 index 0000000..8fb14e3 --- /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)[/^( *)class #{class_name}\n(.*?)^\1end$/m, 2] 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