├── .github ├── CODEOWNERS ├── blunderbuss.yml ├── release-please.yml ├── release-trigger.yml ├── sync-repo-settings.yaml └── workflows │ ├── acceptance-tests-on-emulator.yaml │ ├── acceptance-tests-on-production.yaml │ ├── ci.yaml │ ├── nightly-acceptance-tests-on-emulator.yaml │ ├── nightly-acceptance-tests-on-production.yaml │ ├── nightly-unit-tests.yaml │ ├── rubocop.yaml │ └── samples.yaml ├── .gitignore ├── .kokoro ├── populate-secrets.sh ├── release.cfg ├── release.sh └── trampoline_v2.sh ├── .release-please-manifest.json ├── .rubocop.yml ├── .toys └── release.rb ├── .trampolinerc ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── SECURITY.md ├── acceptance ├── cases │ ├── associations │ │ ├── has_many_associations_test.rb │ │ ├── has_many_through_associations_test.rb │ │ ├── has_one_associations_test.rb │ │ └── has_one_through_associations_test.rb │ ├── interleaved_associations │ │ └── has_many_associations_using_interleaved_test.rb │ ├── migration │ │ ├── change_schema_test.rb │ │ ├── change_table_test.rb │ │ ├── column_attributes_test.rb │ │ ├── column_positioning_test.rb │ │ ├── columns_test.rb │ │ ├── command_recorder_test.rb │ │ ├── create_join_table_test.rb │ │ ├── ddl_batching_test.rb │ │ ├── foreign_key_test.rb │ │ ├── index_test.rb │ │ ├── references_foreign_key_test.rb │ │ ├── references_index_test.rb │ │ ├── references_statements_test.rb │ │ ├── rename_column_test.rb │ │ └── schema_dumper_test.rb │ ├── models │ │ ├── binary_identifiers.rb │ │ ├── calculation_query_test.rb │ │ ├── default_value_test.rb │ │ ├── generated_column_test.rb │ │ ├── insert_all_test.rb │ │ ├── interleave_test.rb │ │ ├── logging_test.rb │ │ ├── mutation_test.rb │ │ └── query_test.rb │ ├── sessions │ │ └── session_not_found_test.rb │ ├── tasks │ │ └── database_tasks_test.rb │ ├── transactions │ │ ├── optimistic_locking_test.rb │ │ ├── read_only_transactions_test.rb │ │ └── read_write_transactions_test.rb │ └── type │ │ ├── all_types_test.rb │ │ ├── binary_test.rb │ │ ├── boolean_test.rb │ │ ├── date_test.rb │ │ ├── date_time_test.rb │ │ ├── float_test.rb │ │ ├── integer_test.rb │ │ ├── json_test.rb │ │ ├── numeric_test.rb │ │ ├── string_test.rb │ │ ├── text_test.rb │ │ └── time_test.rb ├── models │ ├── account.rb │ ├── address.rb │ ├── album.rb │ ├── all_types.rb │ ├── author.rb │ ├── binary_project.rb │ ├── club.rb │ ├── comment.rb │ ├── customer.rb │ ├── department.rb │ ├── firm.rb │ ├── member.rb │ ├── member_type.rb │ ├── membership.rb │ ├── organization.rb │ ├── post.rb │ ├── singer.rb │ ├── string_io.rb │ ├── table_with_sequence.rb │ ├── track.rb │ ├── transaction.rb │ └── user.rb ├── schema │ └── schema.rb ├── test_helper.rb └── test_helpers │ └── with_separate_database.rb ├── activerecord-spanner-adapter.gemspec ├── assets └── solidus-db.png ├── benchmarks ├── README.md ├── Rakefile ├── application.rb ├── config │ ├── database.yml │ └── environment.rb ├── db │ ├── migrate │ │ └── 01_create_tables.rb │ └── schema.rb └── models │ ├── album.rb │ └── singer.rb ├── bin ├── console └── setup ├── examples ├── rails │ └── README.md ├── snippets │ ├── README.md │ ├── Rakefile │ ├── array-data-type │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ └── entity_with_array_types.rb │ ├── auto-generated-primary-key │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── bin │ │ └── create_emulator_instance.rb │ ├── bit-reversed-sequence │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── bulk-insert │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── commit-timestamp │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── config │ │ └── environment.rb │ ├── create-records │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── date-data-type │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ └── singer.rb │ ├── generated-column │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ └── singer.rb │ ├── hints │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── interleaved-tables-before-7.1 │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ ├── singer.rb │ │ │ └── track.rb │ ├── interleaved-tables │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ ├── singer.rb │ │ │ └── track.rb │ ├── isolation-level │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── migrations │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ ├── singer.rb │ │ │ └── track.rb │ ├── mutations │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── optimistic-locking │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── partitioned-dml │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── query-logs │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── quickstart │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── read-only-transactions │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── read-write-transactions │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── stale-reads │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ ├── tags │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ │ └── database.yml │ │ ├── db │ │ │ ├── migrate │ │ │ │ └── 01_create_tables.rb │ │ │ └── seeds.rb │ │ └── models │ │ │ ├── album.rb │ │ │ └── singer.rb │ └── timestamp-data-type │ │ ├── README.md │ │ ├── Rakefile │ │ ├── application.rb │ │ ├── config │ │ └── database.yml │ │ ├── db │ │ ├── migrate │ │ │ └── 01_create_tables.rb │ │ └── seeds.rb │ │ └── models │ │ └── meeting.rb └── solidus │ └── README.md ├── lib ├── active_record │ ├── connection_adapters │ │ ├── spanner │ │ │ ├── column.rb │ │ │ ├── database_statements.rb │ │ │ ├── quoting.rb │ │ │ ├── schema_cache.rb │ │ │ ├── schema_creation.rb │ │ │ ├── schema_definitions.rb │ │ │ ├── schema_dumper.rb │ │ │ ├── schema_statements.rb │ │ │ └── type_metadata.rb │ │ └── spanner_adapter.rb │ ├── tasks │ │ └── spanner_database_tasks.rb │ └── type │ │ └── spanner │ │ ├── array.rb │ │ ├── bytes.rb │ │ ├── spanner_active_record_converter.rb │ │ └── time.rb ├── activerecord-spanner-adapter.rb ├── activerecord_spanner_adapter │ ├── base.rb │ ├── connection.rb │ ├── errors.rb │ ├── foreign_key.rb │ ├── index.rb │ ├── index │ │ └── column.rb │ ├── information_schema.rb │ ├── primary_key.rb │ ├── relation.rb │ ├── table.rb │ ├── table │ │ └── column.rb │ ├── transaction.rb │ └── version.rb ├── arel │ └── visitors │ │ └── spanner.rb └── spanner_client_ext.rb ├── release-please-config.json ├── renovate.json └── test ├── activerecord_spanner_adapter ├── connection_mock_server_test.rb ├── connection_test.rb ├── foreign_key_test.rb ├── index │ └── column_test.rb ├── index_test.rb ├── information_schema_test.rb ├── table │ └── column_test.rb ├── table_test.rb └── transaction_test.rb ├── activerecord_spanner_adapter_test.rb ├── activerecord_spanner_interleaved_table ├── interleaved_tables_test.rb ├── model_helper.rb └── models │ ├── album.rb │ ├── singer.rb │ └── track.rb ├── activerecord_spanner_interleaved_table_version_7_1_and_higher ├── interleaved_tables_test.rb ├── model_helper.rb └── models │ ├── album.rb │ ├── singer.rb │ └── track.rb ├── activerecord_spanner_mock_server ├── aborted_transaction_test.rb ├── base_spanner_mock_server_test.rb ├── cloudspannerlogo.png ├── inline_begin_transaction_test.rb ├── model_helper.rb ├── models │ ├── album.rb │ ├── all_types.rb │ ├── binary_project.rb │ ├── other_adapter.rb │ ├── singer.rb │ ├── string_io.rb │ ├── table_with_commit_timestamp.rb │ ├── table_with_identity.rb │ ├── table_with_sequence.rb │ ├── user.rb │ └── versioned_singer.rb ├── optimistic_locking_test.rb ├── pessimistic_locking_test.rb ├── read_only_transaction_test.rb ├── session_not_found_test.rb └── spanner_active_record_with_mock_server_test.rb ├── migrations_with_mock_server ├── db │ └── migrate │ │ ├── 01_create_singer_and_album_tables.rb │ │ ├── 02_create_tables_without_batching.rb │ │ ├── 03_create_all_native_migration_types.rb │ │ ├── 04_create_singer_and_album_and_track_tables.rb │ │ ├── 05_create_table_with_commit_timestamp.rb │ │ ├── 06_create_table_with_generated_column.rb │ │ ├── 07_create_interleaved_index.rb │ │ ├── 08_create_null_filtered_index.rb │ │ ├── 09_create_index_storing.rb │ │ ├── 10_create_parent_and_child_with_uuid_pk.rb │ │ ├── 11_create_table_with_default_value.rb │ │ ├── 12_create_bit_reversed_sequence.rb │ │ └── 13_create_auto_generated_primary_key.rb ├── migrations_with_mock_server_test.rb └── models │ ├── album.rb │ ├── child_with_uuid_pk.rb │ ├── parent_with_uuid_pk.rb │ ├── singer.rb │ ├── table_with_identity.rb │ ├── table_with_sequence.rb │ └── track.rb ├── mock_server ├── database_admin_mock_server.rb ├── spanner_mock_server.rb ├── spanner_mock_server_test.rb └── statement_result.rb └── test_helper.rb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file. 2 | # This file controls who is tagged for review for any given pull request. 3 | # 4 | # For syntax help see: 5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 6 | 7 | * @googleapis/ruby-team @olavloite @rahul2393 @ansh0l 8 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | assign_issues: 2 | - olavloite 3 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | bumpMinorPreMajor: true 2 | handleGHRelease: true 3 | manifest: true 4 | monorepoTags: true 5 | packageName: activerecord-spanner-adapter 6 | primaryBranch: main 7 | releaseType: ruby-yoshi 8 | -------------------------------------------------------------------------------- /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | rebaseMergeAllowed: true 2 | squashMergeAllowed: true 3 | mergeCommitAllowed: false 4 | branchProtectionRules: 5 | - pattern: main 6 | isAdminEnforced: true 7 | requiredStatusCheckContexts: 8 | - 'cla/google' 9 | requiredApprovingReviewCount: 1 10 | requiresCodeOwnerReviews: true 11 | requiresStrictStatusChecks: true 12 | permissionRules: 13 | - team: ruby-team 14 | permission: push 15 | - team: ruby-admins 16 | permission: admin 17 | -------------------------------------------------------------------------------- /.github/workflows/acceptance-tests-on-emulator.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: acceptance tests on emulator 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | services: 12 | emulator: 13 | image: gcr.io/cloud-spanner-emulator/emulator:latest 14 | ports: 15 | - 9010:9010 16 | - 9020:9020 17 | 18 | strategy: 19 | max-parallel: 4 20 | matrix: 21 | ruby: ["3.1", "3.2", "3.3", "3.4"] 22 | ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] 23 | # Exclude combinations that are not supported. 24 | exclude: 25 | - ruby: "3.1" 26 | ar: "~> 8.0.0" 27 | - ruby: "3.4" 28 | ar: "~> 7.0.0" 29 | - ruby: "3.4" 30 | ar: "~> 7.1.0" 31 | - ruby: "3.4" 32 | ar: "~> 7.2.0" 33 | env: 34 | AR_VERSION: ${{ matrix.ar }} 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Set up Ruby 38 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby 39 | # (see https://github.com/ruby/setup-ruby#versioning): 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | bundler-cache: false 43 | ruby-version: ${{ matrix.ruby }} 44 | - name: Install dependencies 45 | run: bundle install 46 | - name: Run acceptance tests on emulator 47 | run: bundle exec rake acceptance TESTOPTS="-v" 48 | env: 49 | SPANNER_EMULATOR_HOST: localhost:9010 50 | SPANNER_TEST_PROJECT: test-project 51 | SPANNER_TEST_INSTANCE: test-instance 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: ci 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 4 12 | matrix: 13 | ruby: ["3.1", "3.2", "3.3", "3.4"] 14 | ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] 15 | # Exclude combinations that are not supported. 16 | exclude: 17 | - ruby: "3.1" 18 | ar: "~> 8.0.0" 19 | - ruby: "3.4" 20 | ar: "~> 7.0.0" 21 | - ruby: "3.4" 22 | ar: "~> 7.1.0" 23 | - ruby: "3.4" 24 | ar: "~> 7.2.0" 25 | env: 26 | AR_VERSION: ${{ matrix.ar }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Ruby 30 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby 31 | # (see https://github.com/ruby/setup-ruby#versioning): 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | bundler-cache: false 35 | ruby-version: ${{ matrix.ruby }} 36 | - name: Install dependencies 37 | run: bundle install 38 | - name: Run tests 39 | run: bundle exec rake test --trace TESTOPTS="-v" 40 | -------------------------------------------------------------------------------- /.github/workflows/nightly-acceptance-tests-on-emulator.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | # 06:00 UTC 4 | - cron: '0 6 * * *' 5 | workflow_dispatch: 6 | name: nightly acceptance tests on emulator 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | services: 12 | emulator: 13 | image: gcr.io/cloud-spanner-emulator/emulator:latest 14 | ports: 15 | - 9010:9010 16 | - 9020:9020 17 | 18 | strategy: 19 | max-parallel: 4 20 | matrix: 21 | ruby: ["3.1", "3.2", "3.3", "3.4"] 22 | ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] 23 | # Exclude combinations that are not supported. 24 | exclude: 25 | - ruby: "3.1" 26 | ar: "~> 8.0.0" 27 | - ruby: "3.4" 28 | ar: "~> 7.0.0" 29 | - ruby: "3.4" 30 | ar: "~> 7.1.0" 31 | - ruby: "3.4" 32 | ar: "~> 7.2.0" 33 | env: 34 | AR_VERSION: ${{ matrix.ar }} 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Set up Ruby 38 | uses: ruby/setup-ruby@v1 39 | with: 40 | # Disable caching as we are overriding the ActiveRecord below. 41 | bundler-cache: false 42 | ruby-version: ${{ matrix.ruby }} 43 | - name: Install dependencies 44 | run: bundle install 45 | - name: Run acceptance tests on emulator 46 | run: bundle exec rake acceptance 47 | env: 48 | SPANNER_EMULATOR_HOST: localhost:9010 49 | SPANNER_TEST_PROJECT: test-project 50 | SPANNER_TEST_INSTANCE: test-instance 51 | -------------------------------------------------------------------------------- /.github/workflows/nightly-acceptance-tests-on-production.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | # 02:00 UTC 4 | - cron: '0 2 * * *' 5 | name: nightly acceptance tests on production 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | max-parallel: 4 12 | matrix: 13 | ruby: [3.3] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Ruby 17 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby 18 | # (see https://github.com/ruby/setup-ruby#versioning): 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | bundler-cache: true 22 | ruby-version: ${{ matrix.ruby }} 23 | - name: Authenticate Google Cloud 24 | uses: google-github-actions/auth@v2 25 | with: 26 | credentials_json: ${{ secrets.GCP_SA_KEY }} 27 | - name: Setup GCloud 28 | uses: google-github-actions/setup-gcloud@v2 29 | with: 30 | project_id: ${{ secrets.GCP_PROJECT_ID }} 31 | - name: Install dependencies 32 | run: bundle install 33 | - name: Run acceptance tests on production 34 | run: bundle exec rake acceptance\[,,,"exclude cases/migration"\] 35 | env: 36 | SPANNER_TEST_PROJECT: ${{ secrets.GCP_PROJECT_ID }} 37 | SPANNER_TEST_INSTANCE: ruby-activerecord-test 38 | -------------------------------------------------------------------------------- /.github/workflows/nightly-unit-tests.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | # 05:30 UTC 4 | - cron: '30 5 * * *' 5 | workflow_dispatch: 6 | name: nightly-unit-tests 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 4 12 | matrix: 13 | # Run acceptance tests all supported combinations of Ruby and ActiveRecord. 14 | ruby: ["3.1", "3.2", "3.3", "3.4"] 15 | ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] 16 | # Exclude combinations that are not supported. 17 | exclude: 18 | - ruby: "3.1" 19 | ar: "~> 8.0.0" 20 | - ruby: "3.4" 21 | ar: "~> 7.0.0" 22 | - ruby: "3.4" 23 | ar: "~> 7.1.0" 24 | - ruby: "3.4" 25 | ar: "~> 7.2.0" 26 | env: 27 | AR_VERSION: ${{ matrix.ar }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Set up Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | # Disable caching as we are overriding the ActiveRecord below. 34 | bundler-cache: false 35 | ruby-version: ${{ matrix.ruby }} 36 | - name: Install dependencies 37 | run: bundle install 38 | - name: Run tests 39 | run: bundle exec rake test 40 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yaml: -------------------------------------------------------------------------------- 1 | name: rubocop 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: setup ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: '3.3' 20 | - name: cache gems 21 | uses: actions/cache@v4 22 | with: 23 | path: vendor/bundle 24 | key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }} 25 | restore-keys: ${{ runner.os }}-rubocop- 26 | - name: install gems 27 | run: | 28 | bundle config set path vendor/bundle 29 | bundle install --jobs 4 --retry 3 30 | - name: exec rubocop 31 | run: bundle exec rubocop --parallel 32 | -------------------------------------------------------------------------------- /.github/workflows/samples.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | name: samples 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | ruby: ["3.1", "3.2", "3.3", "3.4"] 12 | ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] 13 | # Exclude combinations that are not supported. 14 | exclude: 15 | - ruby: "3.1" 16 | ar: "~> 8.0.0" 17 | - ruby: "3.4" 18 | ar: "~> 7.0.0" 19 | - ruby: "3.4" 20 | ar: "~> 7.1.0" 21 | - ruby: "3.4" 22 | ar: "~> 7.2.0" 23 | env: 24 | AR_VERSION: ${{ matrix.ar }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | bundler-cache: false 31 | ruby-version: ${{ matrix.ruby }} 32 | - name: Install dependencies 33 | run: bundle install 34 | - name: Run samples 35 | run: bundle exec rake all 36 | working-directory: examples/snippets 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Node-related 14 | /node_modules/ 15 | /package-lock.json 16 | 17 | # Used by dotenv library to load environment variables. 18 | # .env 19 | 20 | # Ignore Byebug command history file. 21 | .byebug_history 22 | 23 | ## Specific to RubyMotion: 24 | .dat* 25 | .repl_history 26 | build/ 27 | *.bridgesupport 28 | build-iPhoneOS/ 29 | build-iPhoneSimulator/ 30 | 31 | ## Specific to RubyMotion (use of CocoaPods): 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # vendor/Pods/ 38 | 39 | # Ignore RubyMine project files 40 | .idea 41 | 42 | ## Documentation cache and generated files: 43 | /.yardoc/ 44 | /_yardoc/ 45 | /doc/ 46 | /rdoc/ 47 | 48 | ## Environment normalization: 49 | /.bundle/ 50 | /vendor/bundle 51 | /lib/bundler/man/ 52 | 53 | # for a library or gem, you might want to ignore these files since the code is 54 | # intended to run in multiple environments; otherwise, check them in: 55 | Gemfile.lock 56 | .ruby-version 57 | .ruby-gemset 58 | 59 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 60 | .rvmrc 61 | 62 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 63 | # .rubocop-https?--* 64 | 65 | # Extra 66 | .DS_Store 67 | .DS_STORE 68 | *.diff 69 | *.swp 70 | *.env* 71 | byebug_history 72 | -------------------------------------------------------------------------------- /.kokoro/release.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | # Build logs will be here 4 | action { 5 | define_artifacts { 6 | regex: "**/*sponge_log.xml" 7 | } 8 | } 9 | 10 | # Use the trampoline script to run in docker. 11 | build_file: "ruby-spanner-activerecord/.kokoro/trampoline_v2.sh" 12 | 13 | # Configure the docker image for kokoro-trampoline. 14 | env_vars: { 15 | key: "TRAMPOLINE_IMAGE" 16 | value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/ruby-release" 17 | } 18 | 19 | env_vars: { 20 | key: "TRAMPOLINE_BUILD_FILE" 21 | value: ".kokoro/release.sh" 22 | } 23 | 24 | env_vars: { 25 | key: "SECRET_MANAGER_PROJECT_ID" 26 | value: "cloud-sdk-release-custom-pool" 27 | } 28 | 29 | env_vars: { 30 | key: "SECRET_MANAGER_KEYS" 31 | value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" 32 | } 33 | 34 | # Pick up Rubygems key from internal keystore 35 | before_action { 36 | fetch_keystore { 37 | keystore_resource { 38 | keystore_config_id: 73713 39 | keyname: "rubygems-publish-key" 40 | backend: "blade:keystore-fastconfigpush" 41 | } 42 | } 43 | } 44 | 45 | # Store the packages uploaded to rubygems.org, which 46 | # we can later use to generate SBOMs and attestations. 47 | action { 48 | define_artifacts { 49 | regex: "github/ruby-spanner-activerecord/pkg/*.gem" 50 | strip_prefix: "github" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.kokoro/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # Install gems in the user directory because the default install directory 6 | # is in a read-only location. 7 | export GEM_HOME=$HOME/.gem 8 | export PATH=$GEM_HOME/bin:$PATH 9 | 10 | toys release perform -v --reporter-org=googleapis --force-republish --enable-rad < /dev/null 11 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.3.0" 3 | } 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | google-style: google-style.yml 3 | 4 | AllCops: 5 | Exclude: 6 | - "activerecord-spanner-adapter.gemspec" 7 | - "Gemfile" 8 | - "Rakefile" 9 | - "bin/*" 10 | - "test/**/*" 11 | - "acceptance/**/*" 12 | - "benchmarks/db/schema.rb" 13 | - "examples/snippets/**/db/schema.rb" 14 | - "lib/activerecord-spanner-adapter.rb" 15 | - "vendor/**/*" 16 | 17 | Documentation: 18 | Enabled: false 19 | Layout/HashAlignment: 20 | Enabled: false 21 | Metrics/ClassLength: 22 | Enabled: false 23 | Metrics/ModuleLength: 24 | Enabled: false 25 | Naming/RescuedExceptionsVariableName: 26 | Enabled: false 27 | Naming/MethodParameterName: 28 | Enabled: false 29 | Style/EmptyMethod: 30 | Enabled: false 31 | Style/IfUnlessModifier: 32 | Enabled: false 33 | Style/NumericLiterals: 34 | Enabled: false 35 | Style/NumericPredicate: 36 | Enabled: false 37 | Style/SymbolArray: 38 | Enabled: false 39 | Style/WordArray: 40 | Enabled: false 41 | Style/RegexpLiteral: 42 | Enabled: false 43 | Metrics/MethodLength: 44 | Max: 60 45 | Metrics/BlockLength: 46 | Max: 30 47 | -------------------------------------------------------------------------------- /.toys/release.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if ENV["RUBY_COMMON_TOOLS"] 18 | common_tools_dir = File.expand_path ENV["RUBY_COMMON_TOOLS"] 19 | load File.join(common_tools_dir, "toys", "release") 20 | else 21 | load_git remote: "https://github.com/googleapis/ruby-common-tools.git", 22 | path: "toys/release", 23 | update: true 24 | end 25 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --title=SpannerActiveRecord 3 | --markup markdown 4 | --markup-provider redcarpet 5 | --main README.md 6 | 7 | ./lib/**/*.rb 8 | - 9 | CHANGELOG.md 10 | CODE_OF_CONDUCT.md 11 | LICENSE.txt 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in activerecord-spanner.gemspec 4 | gemspec 5 | 6 | ar_version = ENV.fetch("AR_VERSION", "~> 7.1.0") 7 | gem "activerecord", ar_version 8 | gem "ostruct" 9 | gem "minitest", "~> 5.25.0" 10 | gem "minitest-rg", "~> 5.3.0" 11 | gem "pry", "~> 0.14.2" 12 | gem "pry-byebug", "~> 3.11.0" 13 | # Add sqlite3 for testing for compatibility with other adapters. 14 | gem 'sqlite3' 15 | 16 | # Required for samples and testing. 17 | install_if -> { ar_version.dup.to_s.sub("~>", "").strip < "7.1.0" && !ENV["SKIP_COMPOSITE_PK"] } do 18 | gem "composite_primary_keys" 19 | end 20 | 21 | # Required for samples 22 | gem "docker-api" 23 | gem "sinatra-activerecord" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Google LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. 8 | -------------------------------------------------------------------------------- /acceptance/cases/migration/column_positioning_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | class Migration 13 | class ColumnPositioningTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Migration::TestHelper 15 | 16 | def setup 17 | skip_test_table_create! 18 | super 19 | 20 | connection.ddl_batch do 21 | connection.create_table :testing_columns_position, id: false, force: true do |t| 22 | t.column :first, :integer 23 | t.column :second, :integer 24 | t.column :third, :integer 25 | end 26 | end 27 | end 28 | 29 | def teardown 30 | connection.ddl_batch do 31 | connection.drop_table :testing_columns_position 32 | end rescue nil 33 | ActiveRecord::Base.primary_key_prefix_type = nil 34 | end 35 | 36 | def test_column_positioning 37 | assert_equal %w(first second third), connection.columns(:testing_columns_position).map(&:name) 38 | end 39 | 40 | def test_add_column_with_positioning 41 | connection.ddl_batch do 42 | connection.add_column :testing_columns_position, :fourth, :integer 43 | end 44 | assert_equal %w(first second third fourth), connection.columns(:testing_columns_position).map(&:name) 45 | end 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /acceptance/cases/type/binary_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class BinaryTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "BYTES(MAX)", connection.type_to_sql(:binary) 18 | assert_equal "BYTES(1024)", connection.type_to_sql(:binary, limit: 1024) 19 | end 20 | 21 | def test_set_binary_data_io_in_create 22 | data = StringIO.new "hello" 23 | 24 | record = TestTypeModel.create(data: data) 25 | record.reload 26 | 27 | assert_equal "hello", record.data.read 28 | end 29 | 30 | def test_set_binary_data_byte_string_in_create 31 | data = StringIO.new "hello1" 32 | 33 | record = TestTypeModel.create(data: data.read) 34 | record.reload 35 | 36 | assert_equal "hello1", record.data.read 37 | end 38 | 39 | def test_check_max_limit 40 | str = "a" * 256 41 | 42 | assert_raise(ActiveRecord::StatementInvalid) { 43 | TestTypeModel.create(name: str) 44 | } 45 | end 46 | 47 | def test_set_binary_data_from_file 48 | Tempfile.create do |f| 49 | f << "hello 123" 50 | 51 | record = TestTypeModel.create(file: f) 52 | record.reload 53 | 54 | assert_equal "hello 123", record.file.read 55 | end 56 | end 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /acceptance/cases/type/boolean_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class BooleanTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "BOOL", connection.type_to_sql(:boolean) 18 | end 19 | 20 | def test_set_boolean_value_in_create 21 | record = TestTypeModel.create(active: true) 22 | record.reload 23 | assert_equal true, record.active 24 | 25 | record = TestTypeModel.create(active: false) 26 | record.reload 27 | assert_equal false, record.active 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /acceptance/cases/type/date_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class DateTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "DATE", connection.type_to_sql(:date) 18 | end 19 | 20 | def test_set_date 21 | expected_date = ::Date.new 2020, 1, 31 22 | record = TestTypeModel.new start_date: expected_date 23 | 24 | assert_equal expected_date, record.start_date 25 | 26 | record.save! 27 | record.reload 28 | assert_equal expected_date, record.start_date 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /acceptance/cases/type/date_time_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class DateTimeTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "TIMESTAMP", connection.type_to_sql(:datetime) 18 | assert_equal "TIMESTAMP", connection.type_to_sql(:datetime, limit: 128) 19 | end 20 | 21 | def test_datetime_seconds_precision_applied_to_timestamp 22 | expected_time = ::Time.now 23 | record = TestTypeModel.new start_datetime: expected_time 24 | 25 | assert_equal expected_time, record.start_datetime 26 | assert_equal expected_time.usec, record.start_datetime.usec 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /acceptance/cases/type/float_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class FloatTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "FLOAT64", connection.type_to_sql(:float) 18 | end 19 | 20 | def test_set_float_value_in_create 21 | record = TestTypeModel.create(weight: 123.32199) 22 | record.reload 23 | assert_equal 123.32199, record.weight 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /acceptance/cases/type/integer_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class IntegerTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "INT64", connection.type_to_sql(:integer) 18 | assert_equal "INT64", connection.type_to_sql(:primary_key) 19 | end 20 | 21 | def test_set_integer_value_in_create 22 | record = TestTypeModel.create(length: 123) 23 | 24 | record.reload 25 | assert_equal 123, record.length 26 | end 27 | 28 | def test_casting_models 29 | type = Type::Integer.new 30 | 31 | record = TestTypeModel.create(name: "Google") 32 | assert_nil type.cast(record) 33 | end 34 | 35 | def test_values_out_of_range_can_re_assigned 36 | model = TestTypeModel.new 37 | model.length = 2147483648 38 | model.length = 1 39 | 40 | assert_equal 1, model.length 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /acceptance/cases/type/json_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class DateTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "JSON", connection.type_to_sql(:json) 18 | end 19 | 20 | def test_set_json 21 | expected_hash = {"key"=>"value", "array_key"=>%w[value1 value2]} 22 | record = TestTypeModel.new details: {key: "value", array_key: %w[value1 value2]} 23 | 24 | assert_equal expected_hash, record.details 25 | 26 | record.save! 27 | record.reload 28 | assert_equal expected_hash, record.details 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /acceptance/cases/type/numeric_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class NumericTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "NUMERIC", connection.type_to_sql(:numeric) 18 | end 19 | 20 | def test_set_numeric_value_in_create 21 | record = TestTypeModel.create(price: 9750.99) 22 | record.reload 23 | assert_equal 9750.99, record.price 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /acceptance/cases/type/text_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "test_helper" 10 | 11 | module ActiveRecord 12 | module Type 13 | class TextTest < SpannerAdapter::TestCase 14 | include SpannerAdapter::Types::TestHelper 15 | 16 | def test_convert_to_sql_type 17 | assert_equal "STRING(MAX)", connection.type_to_sql(:text) 18 | assert_equal "STRING(1024)", connection.type_to_sql(:text, limit: 1024) 19 | end 20 | 21 | def test_set_text_in_create 22 | text = "a" * 1000 23 | record = TestTypeModel.create(bio: text) 24 | record.reload 25 | 26 | assert_equal text, record.bio 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /acceptance/models/account.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Account < ActiveRecord::Base 8 | belongs_to :firm 9 | belongs_to :customer 10 | has_many :transactions 11 | 12 | alias_attribute :available_credit, :credit_limit 13 | end 14 | -------------------------------------------------------------------------------- /acceptance/models/address.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Address < ActiveRecord::Base 8 | has_one :author 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | # Register both primary key columns with composite_primary_keys 9 | self.primary_keys = :singerid, :albumid 10 | 11 | # The relationship with singer is not really a foreign key, but an INTERLEAVE IN relationship. We still need to 12 | # use the `foreign_key` attribute to indicate which column to use for the relationship. 13 | belongs_to :singer, foreign_key: :singerid 14 | has_many :tracks, foreign_key: [:singerid, :albumid], dependent: :delete_all 15 | end 16 | -------------------------------------------------------------------------------- /acceptance/models/all_types.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class AllTypes < ActiveRecord::Base 8 | end 9 | -------------------------------------------------------------------------------- /acceptance/models/author.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Author < ActiveRecord::Base 8 | has_many :posts 9 | has_many :comments, through: :posts 10 | belongs_to :organization 11 | end 12 | -------------------------------------------------------------------------------- /acceptance/models/binary_project.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require_relative 'string_io' 10 | 11 | class BinaryProject < ActiveRecord::Base 12 | belongs_to :owner, class_name: 'User' 13 | 14 | before_create :set_uuid 15 | private 16 | 17 | def set_uuid 18 | self.id ||= StringIO.new(SecureRandom.random_bytes(16)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /acceptance/models/club.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Club < ActiveRecord::Base 8 | has_many :memberships 9 | has_many :members, through: :memberships 10 | has_many :favourites, -> { where(memberships: { favourite: true }) }, 11 | through: :memberships, source: :member 12 | end 13 | -------------------------------------------------------------------------------- /acceptance/models/comment.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Comment < ActiveRecord::Base 8 | belongs_to :post, counter_cache: true 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/customer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Customer < ActiveRecord::Base 8 | has_many :accounts 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/department.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Department < ActiveRecord::Base 8 | belongs_to :resource, polymorphic: true 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/firm.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Firm < ActiveRecord::Base 8 | has_one :account 9 | has_many :departments, as: :resource 10 | end 11 | -------------------------------------------------------------------------------- /acceptance/models/member.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Member < ActiveRecord::Base 8 | has_one :membership 9 | has_one :club, through: :membership 10 | has_one :favourite_club, -> { where "memberships.favourite = ?", true }, 11 | through: :membership, source: :club 12 | belongs_to :member_type 13 | end 14 | -------------------------------------------------------------------------------- /acceptance/models/member_type.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class MemberType < ActiveRecord::Base 8 | has_many :members 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/membership.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Membership < ActiveRecord::Base 8 | belongs_to :member 9 | belongs_to :club 10 | end -------------------------------------------------------------------------------- /acceptance/models/organization.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Organization < ActiveRecord::Base 8 | has_many :authors, dependent: :destroy 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/models/post.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Post < ActiveRecord::Base 8 | belongs_to :author 9 | has_many :comments 10 | end 11 | -------------------------------------------------------------------------------- /acceptance/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums, foreign_key: :singerid, dependent: :delete_all 9 | has_many :tracks, foreign_key: :singerid 10 | end 11 | -------------------------------------------------------------------------------- /acceptance/models/string_io.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | # Add equality and hash functions to StringIO to use it for sets. 10 | class StringIO 11 | def ==(o) 12 | o.class == self.class && self.to_base64 == o.to_base64 13 | end 14 | 15 | def eql?(o) 16 | self == o 17 | end 18 | 19 | def hash 20 | to_base64.hash 21 | end 22 | 23 | def to_base64 24 | self.rewind 25 | value = self.read 26 | Base64.strict_encode64 value.force_encoding("ASCII-8BIT") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /acceptance/models/table_with_sequence.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class TableWithSequence < ActiveRecord::Base 8 | self.table_name = :table_with_sequence 9 | self.sequence_name = :test_sequence 10 | end 11 | -------------------------------------------------------------------------------- /acceptance/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Track < ActiveRecord::Base 8 | # Register both primary key columns with composite_primary_keys 9 | self.primary_keys = :singerid, :albumid, :trackid 10 | 11 | belongs_to :album, foreign_key: [:singerid, :albumid] 12 | belongs_to :singer, foreign_key: :singerid, counter_cache: true 13 | 14 | def initialize attributes = nil 15 | super 16 | self.singer ||= self.album&.singer 17 | end 18 | 19 | def album=value 20 | super 21 | self.singer = value&.singer 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /acceptance/models/transaction.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Transaction < ActiveRecord::Base 8 | belongs_to :account, counter_cache: true 9 | end -------------------------------------------------------------------------------- /acceptance/models/user.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require_relative 'string_io' 10 | 11 | class User < ActiveRecord::Base 12 | has_many :binary_projects, foreign_key: :owner_id 13 | 14 | before_create :set_uuid 15 | private 16 | 17 | def set_uuid 18 | self.id ||= StringIO.new(SecureRandom.random_bytes(16)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /acceptance/test_helpers/with_separate_database.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "securerandom" 8 | 9 | require "active_record/tasks/spanner_database_tasks" 10 | 11 | module TestHelpers 12 | module WithSeparateDatabase 13 | attr_reader :connection 14 | 15 | def setup 16 | spanner_adapter_connection.create_database 17 | ActiveRecord::Base.establish_connection connection_config 18 | @connection = ActiveRecord::Base.connection 19 | end 20 | 21 | def teardown 22 | spanner_adapter_connection.database.drop 23 | ActiveRecord::Base.connection_pool.disconnect! 24 | end 25 | 26 | def connection_config 27 | { 28 | "adapter" => "spanner", 29 | "emulator_host" => ENV["SPANNER_EMULATOR_HOST"], 30 | "project" => ENV["SPANNER_TEST_PROJECT"], 31 | "instance" => ENV["SPANNER_TEST_INSTANCE"], 32 | "credentials" => ENV["SPANNER_TEST_KEYFILE"], 33 | "database" => database_id, 34 | } 35 | end 36 | 37 | def database_id 38 | @database_id ||= "ar-test-#{SecureRandom.hex 4}" 39 | end 40 | 41 | def spanner_adapter_connection 42 | @spanner_adapter_connection ||= 43 | ActiveRecordSpannerAdapter::Connection.new( 44 | connection_config.symbolize_keys 45 | ) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /assets/solidus-db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/ruby-spanner-activerecord/0a1020ac60ff9ddaae1634a2178a71bc0de9480c/assets/solidus-db.png -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Benchmarks the Spanner ActiveRecord adapter using a small set of standardized use cases. The benchmarks consists of two 4 | separate runs: 5 | * Execute each use case separately and measure the time needed for each use case. 6 | * Batch all use cases together and execute multiple batches in parallel, measuring the time needed to finish all 7 | batches. The number of batches varies between 1 and 400. The session pool is configured to contain at most 400 8 | connections. 9 | 10 | Change the configuration in the file `config/database.yml` before running the benchmarks. The instance and database in 11 | the configuration must exist. The tables that are needed will automatically be created by the benchmark script. 12 | 13 | Run the benchmark with the command 14 | 15 | ```bash 16 | bundle exec rake run 17 | ``` 18 | -------------------------------------------------------------------------------- /benchmarks/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Runs benchmarks against a Cloud Spanner database." 11 | task :run do |_t, _args| 12 | sh "rake db:migrate" 13 | sh "ruby application.rb" 14 | end 15 | -------------------------------------------------------------------------------- /benchmarks/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | project: 4 | instance: 5 | database: 6 | credentials: 7 | pool: 400 8 | checkout_timeout: 60000 9 | -------------------------------------------------------------------------------- /benchmarks/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "active_record" 8 | require "bundler" 9 | 10 | Dir["../lib/*.rb"].each { |file| require file } 11 | 12 | Bundler.require 13 | -------------------------------------------------------------------------------- /benchmarks/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name, limit: 100 12 | t.string :last_name, null: false, limit: 200 13 | t.string :full_name, null: false, limit: 300, as: "COALESCE(first_name || ' ', '') || last_name", stored: true 14 | t.date :birth_date 15 | t.binary :picture 16 | end 17 | 18 | create_table :albums do |t| 19 | t.string :title 20 | t.date :release_date 21 | t.references :singer, index: false 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /benchmarks/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 1) do 14 | 15 | create_table "albums", force: :cascade do |t| 16 | t.string "title" 17 | t.date "release_date" 18 | t.integer "singer_id", limit: 8 19 | end 20 | 21 | create_table "singers", force: :cascade do |t| 22 | t.string "first_name", limit: 100 23 | t.string "last_name", limit: 200, null: false 24 | t.string "full_name", limit: 300, null: false 25 | t.date "birth_date" 26 | t.binary "picture" 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /benchmarks/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /benchmarks/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "activerecord-spanner-adapter" 5 | 6 | require "active_record" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | require "pry" 12 | require "pry-byebug" # For easy debugging 13 | Pry.start 14 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/snippets/README.md: -------------------------------------------------------------------------------- 1 | # Sample snippets 2 | 3 | This directory contains a number of simple standalone samples that show how to use ActiveRecord with Cloud Spanner. 4 | 5 | ### Running from this directory 6 | The samples can be executed using the following command in this directory: 7 | 8 | ```bash 9 | bundle exec rake run\[\] 10 | ``` 11 | 12 | Example: 13 | 14 | ```bash 15 | bundle exec rake run\[quickstart\] 16 | ``` 17 | 18 | The available samples can be listed using the command 19 | 20 | ```bash 21 | bundle exec rake list 22 | ``` 23 | 24 | ### Running from sample directory 25 | You can also run a sample by calling the following command __in the directory of the sample__: 26 | 27 | ```bash 28 | bundle exec rake run 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Array Data Type 2 | 3 | This example shows how to use the `ARRAY` data type with the Spanner ActiveRecord adapter. The sample uses a single 4 | table that has one column for each possible `ARRAY` data type: 5 | 6 | ```sql 7 | CREATE TABLE entity_with_array_types ( 8 | id INT64 NOT NULL, 9 | col_array_string ARRAY, 10 | col_array_int64 ARRAY, 11 | col_array_float64 ARRAY, 12 | col_array_numeric ARRAY, 13 | col_array_bool ARRAY, 14 | col_array_bytes ARRAY, 15 | col_array_date ARRAY, 16 | col_array_timestamp ARRAY, 17 | ) PRIMARY KEY (id); 18 | ``` 19 | 20 | This schema is created in ActiveRecord as follows: 21 | 22 | ```ruby 23 | create_table :entity_with_array_types do |t| 24 | # Create a table with a column with each possible array type. 25 | t.column :col_array_string, :string, array: true 26 | t.column :col_array_int64, :bigint, array: true 27 | t.column :col_array_float64, :float, array: true 28 | t.column :col_array_numeric, :numeric, array: true 29 | t.column :col_array_bool, :boolean, array: true 30 | t.column :col_array_bytes, :binary, array: true 31 | t.column :col_array_date, :date, array: true 32 | t.column :col_array_timestamp, :datetime, array: true 33 | end 34 | ``` 35 | 36 | ## Running the Sample 37 | 38 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 39 | against that emulator. The emulator will automatically be stopped when the application finishes. 40 | 41 | Run the application with the command 42 | 43 | ```bash 44 | bundle exec rake run 45 | ``` 46 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with array data types in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[array-data-type]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | # Execute the entire migration as one DDL batch. 10 | connection.ddl_batch do 11 | create_table :entity_with_array_types do |t| 12 | # Create a table with a column with each possible array type. 13 | t.column :col_array_string, :string, array: true 14 | t.column :col_array_int64, :bigint, array: true 15 | t.column :col_array_float64, :float, array: true 16 | t.column :col_array_numeric, :numeric, array: true 17 | t.column :col_array_bool, :boolean, array: true 18 | t.column :col_array_bytes, :binary, array: true 19 | t.column :col_array_date, :date, array: true 20 | t.column :col_array_timestamp, :datetime, array: true 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | -------------------------------------------------------------------------------- /examples/snippets/array-data-type/models/entity_with_array_types.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class EntityWithArrayTypes < ActiveRecord::Base 8 | # This entity has one attribute for each possible ARRAY data type in Cloud Spanner. 9 | # col_array_string ARRAY 10 | # col_array_int64 ARRAY 11 | # col_array_float64 ARRAY 12 | # col_array_numeric ARRAY 13 | # col_array_bool ARRAY 14 | # col_array_bytes ARRAY 15 | # col_array_date ARRAY 16 | # col_array_timestamp ARRAY 17 | # 18 | end 19 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with auto-generated-primary keys." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[auto-generated-primary-key]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | default_sequence_kind: BIT_REVERSED_POSITIVE 8 | pool: 5 9 | timeout: 5000 10 | schema_dump: false 11 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[7.1] 8 | def change 9 | connection.default_sequence_kind = "BIT_REVERSED_POSITIVE" 10 | # Execute the entire migration as one DDL batch. 11 | connection.ddl_batch do 12 | create_table :singers, id: false, primary_key: :singerid do |t| 13 | # Use the ':primary_key' data type to create an auto-generated primary key column. 14 | t.column :singerid, :primary_key, primary_key: true, null: false 15 | t.string :first_name 16 | t.string :last_name 17 | end 18 | 19 | create_table :albums, primary_key: [:singerid, :albumid], id: false do |t| 20 | # Interleave the `albums` table in the parent table `singers`. 21 | t.interleave_in :singers 22 | t.integer :singerid, null: false 23 | # Use the ':primary_key' data type to create an auto-generated primary key column. 24 | t.column :albumid, :primary_key, null: false 25 | t.string :title 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly] 12 | last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou] 13 | 14 | adjectives = %w[daily happy blue generous cooked bad open] 15 | nouns = %w[windows potatoes bank street tree glass bottle] 16 | 17 | # NOTE: We do not use mutations to insert these rows, because letting the database generate the primary key means that 18 | # we rely on a `THEN RETURN id` clause in the insert statement. This is only supported for DML statements, and not for 19 | # mutations. 20 | ActiveRecord::Base.transaction do 21 | singers = [] 22 | 5.times do 23 | singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample) 24 | end 25 | 26 | albums = [] 27 | 20.times do 28 | singer = singers.sample 29 | albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `singers` is `singerid`. 10 | belongs_to :singer, foreign_key: :singerid 11 | end 12 | -------------------------------------------------------------------------------- /examples/snippets/auto-generated-primary-key/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `albums` is (`singerid`, `albumid`). 10 | has_many :albums, foreign_key: :singerid 11 | end 12 | -------------------------------------------------------------------------------- /examples/snippets/bin/create_emulator_instance.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "google/cloud/spanner" 8 | 9 | spanner = Google::Cloud::Spanner.new project: "test-project", emulator_host: "localhost:9010" 10 | job = spanner.create_instance "test-instance", 11 | name: "Test Instance", 12 | config: "emulator-config", 13 | nodes: 1 14 | job.wait_until_done! 15 | 16 | instance = spanner.instance "test-instance" 17 | job = instance.create_database "testdb", statements: [] 18 | job.wait_until_done! 19 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with bit-reversed sequences." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[bit-reversed-sequence]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[7.1] 8 | def change 9 | # Execute the entire migration as one DDL batch. 10 | connection.ddl_batch do 11 | connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')" 12 | 13 | # Explicitly define the primary key. 14 | create_table :singers, id: false, primary_key: :singerid do |t| 15 | t.integer :singerid, primary_key: true, null: false, 16 | default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" } 17 | t.string :first_name 18 | t.string :last_name 19 | end 20 | 21 | create_table :albums, primary_key: [:singerid, :albumid], id: false do |t| 22 | # Interleave the `albums` table in the parent table `singers`. 23 | t.interleave_in :singers 24 | t.integer :singerid, null: false 25 | t.integer :albumid, null: false 26 | t.string :title 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly] 12 | last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou] 13 | 14 | adjectives = %w[daily happy blue generous cooked bad open] 15 | nouns = %w[windows potatoes bank street tree glass bottle] 16 | 17 | # NOTE: We do not use mutations to insert these rows, because letting the database generate the primary key means that 18 | # we rely on a `THEN RETURN id` clause in the insert statement. This is only supported for DML statements, and not for 19 | # mutations. 20 | ActiveRecord::Base.transaction do 21 | singers = [] 22 | 5.times do 23 | singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample) 24 | end 25 | 26 | albums = [] 27 | 20.times do 28 | singer = singers.sample 29 | albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `singers` is `singerid`. 10 | belongs_to :singer, foreign_key: :singerid 11 | end 12 | -------------------------------------------------------------------------------- /examples/snippets/bit-reversed-sequence/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | # Set the sequence name so the ActiveRecord provider knows that it should let the database generate the primary key 9 | # value and return it using a `THEN RETURN id` clause. 10 | self.sequence_name = :singer_sequence 11 | 12 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 13 | # The primary key of `albums` is (`singerid`, `albumid`). 14 | has_many :albums, foreign_key: :singerid 15 | end 16 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Bulk Insert 2 | 3 | It is important to use the correct strategy when bulk creating new records in Spanner using ActiveRecord. 4 | ActiveRecord will by default create a separate insert statement for each entity that you create. The Spanner 5 | ActiveRecord adapter is able to translate this into a set of insert Mutations instead of a list of insert DML 6 | statements, which will execute a lot faster. 7 | 8 | There are two ways to achieve this: 9 | 1. Create a batch of entities without an explicit transaction. 10 | 2. Use a read/write transaction with isolation level `:buffered_mutations` 11 | 12 | See also the `Mutations` sample in this repository. 13 | 14 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 15 | against that emulator. The emulator will automatically be stopped when the application finishes. 16 | 17 | Run the application with the command 18 | 19 | ```bash 20 | bundle exec rake run 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to best bulk insert new records in Spanner using ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[bulk-insert]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/bulk-insert/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Commit Timestamps 2 | 3 | This example shows how to set an attribute to the [commit timestamp of a transaction](https://cloud.google.com/spanner/docs/commit-timestamp) 4 | using the Spanner ActiveRecord adapter. Setting the commit timestamp is different from the built-in 5 | `created_at`/`updated_at` [feature](https://guides.rubyonrails.org/active_record_basics.html#schema-conventions) of 6 | ActiveRecord. A `created_at`/`updated_at` column that is managed by ActiveRecord will be set to the date/time of the 7 | __client__ when the record is created/updated. The commit timestamp of a transaction is generated by Cloud Spanner and 8 | is set by the server when the transaction is committed. All records that are created or updated in the same transaction 9 | will be assigned the exact same commit timestamp. 10 | 11 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 12 | against that emulator. The emulator will automatically be stopped when the application finishes. 13 | 14 | Run the application with the command 15 | 16 | ```bash 17 | bundle exec rake run 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to fill a commit timestamp column in Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[commit-timestamp]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | # Create a `last_updated` column that supports server side commit timestamps. 14 | t.datetime :last_updated, allow_commit_timestamp: true 15 | end 16 | 17 | create_table :albums do |t| 18 | t.string :title 19 | t.numeric :marketing_budget 20 | t.references :singer, index: false, foreign_key: true 21 | # Create a `last_updated` column that supports server side commit timestamps. 22 | t.datetime :last_updated, allow_commit_timestamp: true 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/commit-timestamp/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "logger" # https://github.com/rails/rails/issues/54260 8 | require "active_record" 9 | require "bundler" 10 | 11 | Dir["../../lib/*.rb"].each { |file| require file } 12 | 13 | if ActiveRecord.version >= Gem::Version.create("7.2.0") 14 | ActiveRecord::ConnectionAdapters.register "spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter" 15 | end 16 | 17 | Bundler.require 18 | 19 | ActiveRecord::Base.establish_connection( 20 | adapter: "spanner", 21 | emulator_host: "localhost:9010", 22 | project: "test-project", 23 | instance: "test-instance", 24 | database: "testdb" 25 | ) 26 | ActiveRecord::Base.logger = nil 27 | -------------------------------------------------------------------------------- /examples/snippets/create-records/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Create Records 2 | 3 | This sample shows how to create and save entities using ActiveRecord. 4 | 5 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 6 | against that emulator. The emulator will automatically be stopped when the application finishes. 7 | 8 | Run the application with the command 9 | 10 | ```bash 11 | bundle exec rake run 12 | ``` 13 | -------------------------------------------------------------------------------- /examples/snippets/create-records/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to create one or more records in Spanner using ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[create-records]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/create-records/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run 14 | # Creating a single record without an explicit transaction will automatically save it to the database. 15 | # It is not recommended to call Entity.create repeatedly to insert multiple records, as each call will 16 | # use a separate Spanner transaction. Instead, multiple records should be created by passing an array of 17 | # entities to the Entity.create method. 18 | singer = Singer.create first_name: "Dave", last_name: "Allison" 19 | puts "" 20 | puts "Created singer #{singer.first_name} #{singer.last_name} with id #{singer.id}" 21 | puts "" 22 | 23 | # Creating multiple records without an explicit transaction will automatically save all the records using 24 | # one Spanner transaction and return the ids of the created records. This is the recommended way to create 25 | # a batch of entities. 26 | singers = Singer.create [ 27 | { first_name: "Alice", last_name: "Wendelson" }, 28 | { first_name: "Nick", last_name: "Rainbow" }, 29 | { first_name: "Elena", last_name: "Quick" } 30 | ] 31 | puts "Created a batch of #{singers.length} singers:" 32 | singers.each do |s| 33 | puts " Created singer #{s.first_name} #{s.last_name} with id #{s.id}" 34 | end 35 | end 36 | end 37 | 38 | Application.run 39 | -------------------------------------------------------------------------------- /examples/snippets/create-records/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/create-records/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/create-records/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | -------------------------------------------------------------------------------- /examples/snippets/create-records/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/create-records/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Date Data Type 2 | 3 | This example shows how to use the `DATE` data type with the Spanner ActiveRecord adapter. A `DATE` is a 4 | timezone-independent date. It does not designate a specific point in time, such as UTC midnight of the date. 5 | If you create a timezone-specific date/time object in Ruby and assign it to a `DATE` attribute, all time and timezone 6 | information will be lost after saving and reloading the object. 7 | 8 | Use the `TIMESTAMP` data type for attributes that represent a specific point in time. 9 | 10 | ## Running the Sample 11 | 12 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 13 | against that emulator. The emulator will automatically be stopped when the application finishes. 14 | 15 | Run the application with the command 16 | 17 | ```bash 18 | bundle exec rake run 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with the DATE data type in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[date-data-type]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | 11 | class Application 12 | def self.run 13 | # Get all singers order by birthdate 14 | puts "" 15 | puts "Listing all singers order by birth date:" 16 | Singer.all.order(:birth_date).each do |singer| 17 | puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}" 18 | end 19 | 20 | # Update the birthdate of a random singer using the current system time. Any time and timezone information will be 21 | # lost after saving the record as a DATE only contains the year, month and day-of-month information. 22 | singer = Singer.all.sample 23 | singer.update birth_date: Time.now 24 | singer.reload 25 | puts "" 26 | puts "Updated birth date to current system time:" 27 | puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}" 28 | end 29 | end 30 | 31 | Application.run 32 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | # A date in Cloud Spanner represents a timezone independent date. It does not designate a specific point in 14 | # time, such as for example midnight UTC of the specified date. 15 | # See https://cloud.google.com/spanner/docs/data-definition-language#data_types for more information. 16 | t.date :birth_date 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | # 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | 10 | first_names = %w[Nelson Todd William Alex Dominique Adenoid Steve Nathan Beverly Annie Amy Norma Diana Regan Phyllis] 11 | last_names = %w[Thornton Morgan Lawson Collins Frost Maxwell Sanders Fleming Jones Webb Walker French Montgomery Quinn] 12 | 13 | 30.times do 14 | Singer.create first_name: first_names.sample, last_name: last_names.sample, 15 | birth_date: Date.new(rand(1920...2010), rand(1...12), rand(1...28)) 16 | end 17 | -------------------------------------------------------------------------------- /examples/snippets/date-data-type/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | end 9 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Generated Columns 2 | 3 | This example shows how to use generated columns with the Spanner ActiveRecord adapter. 4 | 5 | See https://cloud.google.com/spanner/docs/generated-column/how-to for more information on generated columns. 6 | 7 | This example uses the following table schema: 8 | 9 | ```sql 10 | CREATE TABLE singers ( 11 | id INT64 NOT NULL, 12 | first_name STRING(100), 13 | last_name STRING(200) NOT NULL, 14 | full_name STRING(300) NOT NULL AS (COALESCE(first_name || ' ', '') || last_name) STORED, 15 | ) PRIMARY KEY (id); 16 | ``` 17 | 18 | This schema can be created in ActiveRecord as follows: 19 | 20 | ```ruby 21 | create_table :singers do |t| 22 | t.string :first_name, limit: 100 23 | t.string :last_name, limit: 200, null: false 24 | t.string :full_name, limit: 300, null: false, as: "COALESCE(first_name || ' ', '') || last_name", stored: true 25 | end 26 | ``` 27 | 28 | The `full_name` attribute will automatically be set by Cloud Spanner, and it is not allowed to set a value for the 29 | attribute when creating a record in ActiveRecord, or to update the value of an existing record. Instead, only the 30 | `first_name` and `last_name` attributes should be set. 31 | 32 | ## Running the Sample 33 | 34 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 35 | against that emulator. The emulator will automatically be stopped when the application finishes. 36 | 37 | Run the application with the command 38 | 39 | ```bash 40 | bundle exec rake run 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with generated columns in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[generated-column]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | 11 | class Application 12 | def self.run 13 | puts "" 14 | puts "Listing all singers:" 15 | Singer.all.order("last_name, first_name").each do |singer| 16 | puts singer.full_name 17 | end 18 | 19 | # Create a new singer and print out the full name. 20 | singer = Singer.create first_name: "Alice", last_name: "Rees" 21 | singer.reload 22 | puts "" 23 | puts "Singer created: #{singer.full_name}" 24 | 25 | # Update the last name of the singer and print out the full name. 26 | singer.update last_name: "Rees-Goodwin" 27 | singer.reload 28 | puts "" 29 | puts "Singer updated: #{singer.full_name}" 30 | end 31 | end 32 | 33 | Application.run 34 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name, limit: 100 12 | t.string :last_name, limit: 200, null: false 13 | 14 | # Create a generated column that contains the full name of the singer. This will be the concatenated first name 15 | # and last name of the singer, or only the last name if the first name is null. The `as` keyword is what 16 | # instructs the Spanner ActiveRecord adapter to create a generated column. Note the `stored` option that is set 17 | # to true. This is required, as Cloud Spanner (currently) does not support non-stored generated columns. 18 | # See also https://cloud.google.com/spanner/docs/generated-column/how-to for more information. 19 | t.string :full_name, limit: 300, null: false, as: "COALESCE(first_name || ' ', '') || last_name", stored: true 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | 10 | first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly] 11 | last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou] 12 | 13 | # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML. 14 | ActiveRecord::Base.transaction isolation: :buffered_mutations do 15 | 10.times do 16 | Singer.create first_name: first_names.sample, last_name: last_names.sample 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/snippets/generated-column/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | end 9 | -------------------------------------------------------------------------------- /examples/snippets/hints/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Query Hints 2 | 3 | This example shows how to use query hints Spanner ActiveRecord adapter. Statement hints and 4 | table hints can be specified using the optimizer_hints method. Join hints must be specified 5 | in a join string. 6 | 7 | See https://cloud.google.com/spanner/docs/query-syntax#sql_syntax for more information on 8 | the supported query hints. 9 | 10 | ## Running the Sample 11 | 12 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 13 | against that emulator. The emulator will automatically be stopped when the application finishes. 14 | 15 | Run the application with the command 16 | 17 | ```bash 18 | bundle exec rake run 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/snippets/hints/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with generated columns in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[hints]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/hints/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/hints/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name, limit: 100 12 | t.string :last_name, limit: 200, null: false 13 | t.string :full_name, limit: 300, null: false, as: "COALESCE(first_name || ' ', '') || last_name", stored: true 14 | t.index [:full_name], name: "index_singers_on_full_name" 15 | end 16 | 17 | create_table :albums do |t| 18 | t.string :title 19 | t.references :singer, index: false, foreign_key: true 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/snippets/hints/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly] 12 | last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou] 13 | 14 | adjectives = %w[daily happy blue generous cooked bad open] 15 | nouns = %w[windows potatoes bank street tree glass bottle] 16 | 17 | # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML. 18 | ActiveRecord::Base.transaction isolation: :buffered_mutations do 19 | singers = [] 20 | 5.times do 21 | singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample) 22 | end 23 | 24 | albums = [] 25 | 20.times do 26 | singer = singers.sample 27 | albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/snippets/hints/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/hints/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables-before-7.1/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with interleaved tables in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[interleaved-tables-before-7.1]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables-before-7.1/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables-before-7.1/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | class Album < ActiveRecord::Base 10 | # Use the `composite_primary_key` gem to create a composite primary key definition for the model. 11 | self.primary_keys = :singerid, :albumid 12 | 13 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 14 | # The primary key of `singers` is `singerid`. 15 | belongs_to :singer, foreign_key: :singerid 16 | 17 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 18 | # The primary key of `albums` is (`singerid`, `albumid`). 19 | has_many :tracks, foreign_key: [:singerid, :albumid] 20 | end 21 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables-before-7.1/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `albums` is (`singerid`, `albumid`). 10 | has_many :albums, foreign_key: :singerid 11 | 12 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 13 | # The primary key of `tracks` is [`singerid`, `albumid`, `trackid`]. 14 | # The `singerid` column can be used to associate tracks with a singer without the need to go through albums. 15 | # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child 16 | # table of `albums` which has primary key (`singerid`, `albumid`). 17 | has_many :tracks, foreign_key: :singerid 18 | end 19 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables-before-7.1/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Track < ActiveRecord::Base 8 | # Use the `composite_primary_key` gem to create a composite primary key definition for the model. 9 | self.primary_keys = :singerid, :albumid, :trackid 10 | 11 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`). 12 | belongs_to :album, foreign_key: [:singerid, :albumid] 13 | 14 | # `tracks` also has a `singerid` column should be used to associate a Track with a Singer. 15 | belongs_to :singer, foreign_key: :singerid 16 | 17 | # Override the default initialize method to automatically set the singer attribute when an album is given. 18 | def initialize attributes = nil 19 | super 20 | self.singer ||= album&.singer 21 | end 22 | 23 | def album=value 24 | super 25 | # Ensure the singer of this track is equal to the singer of the album that is set. 26 | self.singer = value&.singer 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with interleaved tables in ActiveRecord 7.1 and later." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[interleaved-tables]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[7.1] 8 | def change 9 | # Execute the entire migration as one DDL batch. 10 | connection.ddl_batch do 11 | # Explicitly define the primary key. 12 | create_table :singers, id: false, primary_key: :singerid do |t| 13 | t.integer :singerid 14 | t.string :first_name 15 | t.string :last_name 16 | end 17 | 18 | create_table :albums, primary_key: [:singerid, :albumid], id: false do |t| 19 | # Interleave the `albums` table in the parent table `singers`. 20 | t.interleave_in :singers 21 | t.integer :singerid 22 | t.integer :albumid 23 | t.string :title 24 | end 25 | 26 | create_table :tracks, primary_key: [:singerid, :albumid, :trackid], id: false do |t| 27 | # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an 28 | # album when an album is deleted. 29 | t.interleave_in :albums, :cascade 30 | t.integer :singerid 31 | t.integer :albumid 32 | t.integer :trackid 33 | t.string :title 34 | t.numeric :duration 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `singers` is `singerid`. 10 | belongs_to :singer, foreign_key: :singerid 11 | 12 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 13 | # The primary key of `albums` is (`singerid`, `albumid`). 14 | if ActiveRecord::VERSION::MAJOR >= 8 15 | has_many :tracks, foreign_key: [:singerid, :albumid] 16 | else 17 | # Rails 7.1 requires using query_constraints to define a composite foreign key. 18 | has_many :tracks, query_constraints: [:singerid, :albumid] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | # `albums` is defined as INTERLEAVE IN PARENT `singers`. 9 | # The primary key of `albums` is (`singerid`, `albumid`). 10 | has_many :albums, foreign_key: :singerid 11 | 12 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 13 | # The primary key of `tracks` is [`singerid`, `albumid`, `trackid`]. 14 | # The `singerid` column can be used to associate tracks with a singer without the need to go through albums. 15 | # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child 16 | # table of `albums` which has primary key (`singerid`, `albumid`). 17 | has_many :tracks, foreign_key: :singerid 18 | end 19 | -------------------------------------------------------------------------------- /examples/snippets/interleaved-tables/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Track < ActiveRecord::Base 8 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 9 | # The primary key of `albums` is (`singerid`, `albumid`). 10 | # Rails 7.1 requires a composite primary key in a belongs_to relationship to be specified as query_constraints. 11 | if ActiveRecord::VERSION::MAJOR >= 8 12 | belongs_to :album, foreign_key: [:singerid, :albumid] 13 | else 14 | belongs_to :album, query_constraints: [:singerid, :albumid] 15 | end 16 | 17 | # `tracks` also has a `singerid` column that can be used to associate a Track with a Singer. 18 | belongs_to :singer, foreign_key: :singerid 19 | 20 | # Override the default initialize method to automatically set the singer attribute when an album is given. 21 | def initialize attributes = nil 22 | super 23 | self.singer ||= album&.singer 24 | end 25 | 26 | def album=value 27 | super 28 | # Ensure the singer of this track is equal to the singer of the album that is set. 29 | self.singer = value&.singer 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Isolation Level 2 | 3 | This example shows how to use a specific isolation level for read/write transactions 4 | using the Spanner ActiveRecord adapter. 5 | 6 | You can specify the isolation level in two ways: 7 | 8 | 1. Set a default in the database configuration: 9 | 10 | ```yaml 11 | development: 12 | adapter: spanner 13 | emulator_host: localhost:9010 14 | project: test-project 15 | instance: test-instance 16 | database: testdb 17 | isolation_level: :serializable, 18 | pool: 5 19 | timeout: 5000 20 | schema_dump: false 21 | ``` 22 | 23 | 2. Specify the isolation level for a specific transaction. This will override any 24 | default that is set in the database configuration. 25 | 26 | ```ruby 27 | ActiveRecord::Base.transaction isolation: :repeatable_read do 28 | # Execute transaction code... 29 | end 30 | ``` 31 | 32 | The sample will automatically start a Spanner Emulator in a Docker container and execute the sample 33 | against that emulator. The emulator will automatically be stopped when the application finishes. 34 | 35 | Run the application with the command 36 | 37 | ```bash 38 | bundle exec rake run 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to specify a transaction isolation level for Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[isolation-level]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run # rubocop:disable Metrics/AbcSize 14 | from_album = nil 15 | to_album = nil 16 | # Execute a read/write transaction using isolation level repeatable read. 17 | puts "Executing a read/write transaction using isolation level repeatable read" 18 | ActiveRecord::Base.transaction isolation: :repeatable_read do 19 | # Transfer a marketing budget of 10,000 from one album to another. 20 | from_album = Album.all.sample 21 | to_album = Album.where.not(id: from_album.id).sample 22 | 23 | puts "" 24 | puts "Transferring 10,000 marketing budget from #{from_album.title} (#{from_album.marketing_budget}) " \ 25 | "to #{to_album.title} (#{to_album.marketing_budget})" 26 | from_album.update marketing_budget: from_album.marketing_budget - 10000 27 | to_album.update marketing_budget: to_album.marketing_budget + 10000 28 | end 29 | puts "" 30 | puts "Budgets after update:" 31 | puts "Marketing budget #{from_album.title}: #{from_album.reload.marketing_budget}" 32 | puts "Marketing budget #{to_album.title}: #{to_album.reload.marketing_budget}" 33 | end 34 | end 35 | 36 | Application.run 37 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | isolation_level: :serializable, 8 | pool: 5 9 | timeout: 5000 10 | schema_dump: false 11 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.numeric :marketing_budget 18 | t.references :singer, index: false, foreign_key: true 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | budgets = [15000, 25000, 10000, 20000, 30000, 12000, 13000] 17 | 18 | 5.times do 19 | Singer.create first_name: first_names.sample, last_name: last_names.sample 20 | end 21 | 22 | 20.times do 23 | singer_id = Singer.all.sample.id 24 | Album.create title: "#{adjectives.sample} #{nouns.sample}", marketing_budget: budgets.sample, singer_id: singer_id 25 | end 26 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/isolation-level/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Migrations 2 | 3 | This example shows the best way to execute migrations with the Spanner ActiveRecord adapter. 4 | 5 | It is [strongly recommended](https://cloud.google.com/spanner/docs/schema-updates#best-practices) that you limit the 6 | frequency of schema updates in Cloud Spanner, and that schema changes are batched together whenever possible. The 7 | Spanner ActiveRecord adapter supports batching DDL statements together using the `connection.ddl_batch` method. This 8 | method accepts a block of DDL statements that will be sent to Cloud Spanner as one batch. It is recommended that 9 | migrations are grouped together in one or in a limited number of batches for optimal performance. 10 | 11 | This example shows how to create three tables in one batch: 12 | 13 | ```ruby 14 | # Execute the entire migration as one DDL batch. 15 | connection.ddl_batch do 16 | create_table :singers do |t| 17 | t.string :first_name 18 | t.string :last_name 19 | end 20 | 21 | create_table :albums do |t| 22 | t.string :title 23 | t.references :singers 24 | end 25 | 26 | create_table :tracks do |t| 27 | t.string :title 28 | t.numeric :duration 29 | t.references :albums 30 | end 31 | end 32 | ``` 33 | 34 | ## Running the Sample 35 | 36 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 37 | against that emulator. The emulator will automatically be stopped when the application finishes. 38 | 39 | Run the application with the command 40 | 41 | ```bash 42 | bundle exec rake run 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/snippets/migrations/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing the best practice for executing migrations (schema updates) on Cloud Spanner in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[migrations]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/migrations/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | 10 | class Application 11 | def self.run 12 | puts "" 13 | puts "Created database with the following tables:" 14 | sql = "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_CATALOG='' AND TABLE_SCHEMA=''" 15 | tables = ActiveRecord::Base.connection.raw_connection.execute_query sql 16 | tables.rows.each do |row| 17 | puts row[:TABLE_NAME] 18 | end 19 | end 20 | end 21 | 22 | Application.run 23 | -------------------------------------------------------------------------------- /examples/snippets/migrations/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/migrations/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | # Execute the entire migration as one DDL batch. 10 | connection.ddl_batch do 11 | create_table :singers do |t| 12 | t.string :first_name 13 | t.string :last_name 14 | end 15 | 16 | create_table :albums do |t| 17 | t.string :title 18 | t.references :singers 19 | end 20 | 21 | create_table :tracks do |t| 22 | t.string :title 23 | t.numeric :duration 24 | t.references :albums 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /examples/snippets/migrations/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | -------------------------------------------------------------------------------- /examples/snippets/migrations/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | has_many :tracks 10 | end 11 | -------------------------------------------------------------------------------- /examples/snippets/migrations/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | has_many :tracks, through: :albums 10 | end 11 | -------------------------------------------------------------------------------- /examples/snippets/migrations/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Track < ActiveRecord::Base 8 | belongs_to :album 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/mutations/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to execute a read/write transaction using Mutations instead of DML with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[mutations]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/mutations/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/mutations/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.numeric :marketing_budget 18 | t.references :singer, index: false, foreign_key: true 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/snippets/mutations/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | budgets = [15000, 25000, 10000, 20000, 30000, 12000, 13000] 17 | 18 | 5.times do 19 | Singer.create first_name: first_names.sample, last_name: last_names.sample 20 | end 21 | 22 | 20.times do 23 | singer_id = Singer.all.sample.id 24 | Album.create title: "#{adjectives.sample} #{nouns.sample}", marketing_budget: budgets.sample, singer_id: singer_id 25 | end 26 | -------------------------------------------------------------------------------- /examples/snippets/mutations/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/mutations/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Optimistic Locking 2 | 3 | This example shows how to use optimistic locking with the Spanner ActiveRecord adapter. 4 | 5 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 6 | against that emulator. The emulator will automatically be stopped when the application finishes. 7 | 8 | Run the application with the command 9 | 10 | ```bash 11 | bundle exec rake run 12 | ``` 13 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to use optimistic locking with the Cloud Spanner ActiveRecord adapter." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[optimistic-locking]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run 14 | # Get a random singer and then update the singer in a separate thread. 15 | # This simulates a concurrent update of the same record by two different processes. 16 | singer = Singer.all.sample 17 | 18 | puts "" 19 | puts "Singer #{singer.first_name} #{singer.last_name} with version #{singer.lock_version} loaded" 20 | 21 | t = Thread.new do 22 | # Load the singer in the separate thread into a separate variable. 23 | singer2 = Singer.find singer.id 24 | singer2.update last_name: "Rashford" 25 | puts "" 26 | puts "Updated the last name of the singer to #{singer2.last_name}" 27 | end 28 | t.join 29 | 30 | # Now try to update the singer in the main thread. This will fail, as the lock_version number has been increased 31 | # by the update in the separate thread. 32 | begin 33 | singer.update last_name: "Drake" 34 | rescue ActiveRecord::StaleObjectError 35 | puts "" 36 | puts "Updating the singer in the main thread failed with a StaleObjectError" 37 | end 38 | 39 | singer.reload 40 | puts "Reloaded singer data: #{singer.first_name} #{singer.last_name}, version: #{singer.lock_version}" 41 | end 42 | end 43 | 44 | Application.run 45 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | # `lock_version` is the default name for an optimistic lock version column in ActiveRecord. 14 | t.integer :lock_version 15 | end 16 | 17 | create_table :albums do |t| 18 | t.string :title 19 | t.numeric :marketing_budget 20 | t.references :singer, index: false, foreign_key: true 21 | # `lock_version` is the default name for an optimistic lock version column in ActiveRecord. 22 | t.integer :lock_version 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | budgets = [15000, 25000, 10000, 20000, 30000, 12000, 13000] 17 | 18 | 5.times do 19 | Singer.create first_name: first_names.sample, last_name: last_names.sample 20 | end 21 | 22 | 20.times do 23 | singer_id = Singer.all.sample.id 24 | Album.create title: "#{adjectives.sample} #{nouns.sample}", marketing_budget: budgets.sample, singer_id: singer_id 25 | end 26 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/optimistic-locking/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Partitioned DML 2 | 3 | This example shows how to use Partitioned DML with the Spanner ActiveRecord adapter. 4 | 5 | See https://cloud.google.com/spanner/docs/dml-partitioned for more information on Partitioned DML. 6 | 7 | ## Running the Sample 8 | 9 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 10 | against that emulator. The emulator will automatically be stopped when the application finishes. 11 | 12 | Run the application with the command 13 | 14 | ```bash 15 | bundle exec rake run 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with Partitioned DML in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[partitioned-dml]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run 14 | singer_count = Singer.all.count 15 | album_count = Album.all.count 16 | puts "" 17 | puts "Singers in the database: #{singer_count}" 18 | puts "Albums in the database: #{album_count}" 19 | 20 | puts "" 21 | puts "Deleting all albums in the database using Partitioned DML" 22 | # Note that a Partitioned DML transaction can contain ONLY ONE DML statement. 23 | # If we want to delete all data in two different tables, we need to do so in two different PDML transactions. 24 | Album.transaction isolation: :pdml do 25 | count = Album.delete_all 26 | puts "Deleted #{count} albums" 27 | end 28 | 29 | puts "" 30 | puts "Deleting all singers in the database using Partitioned DML" 31 | Singer.transaction isolation: :pdml do 32 | count = Singer.delete_all 33 | puts "Deleted #{count} singers" 34 | end 35 | 36 | singer_count = Singer.all.count 37 | album_count = Album.all.count 38 | puts "" 39 | puts "Singers in the database: #{singer_count}" 40 | puts "Albums in the database: #{album_count}" 41 | end 42 | end 43 | 44 | Application.run 45 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name, limit: 100 12 | t.string :last_name, limit: 200, null: false 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly] 12 | last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou] 13 | 14 | adjectives = %w[daily happy blue generous cooked bad open] 15 | nouns = %w[windows potatoes bank street tree glass bottle] 16 | 17 | # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML. 18 | ActiveRecord::Base.transaction isolation: :buffered_mutations do 19 | singers = [] 20 | 5.times do 21 | singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample) 22 | end 23 | 24 | albums = [] 25 | 20.times do 26 | singer = singers.sample 27 | albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/partitioned-dml/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to use automatic query log tagging on Cloud Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[query-logs]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | 17 | 5.times do 18 | Singer.create first_name: first_names.sample, last_name: last_names.sample 19 | end 20 | 21 | 20.times do 22 | singer_id = Singer.all.sample.id 23 | Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/query-logs/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Quickstart 2 | 3 | This sample shows how to create and use a very simple Cloud Spanner database with ActiveRecord. 4 | The database consists of two tables: 5 | - Singers 6 | - Albums 7 | 8 | Albums references Singers with a [foreign key](https://cloud.google.com/spanner/docs/foreign-keys/overview). 9 | 10 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 11 | against that emulator. The emulator will automatically be stopped when the application finishes. 12 | 13 | This sample will: 14 | 1. Create the sample database by calling `rake db:migrate`. This will execute the migrations in the folder `db/migrate`. 15 | 2. Fill the sample database with some data by calling `rake db:seed`. This will execute the script in `db/seeds.rb`. 16 | The seed script fills the database with 10 random singers and 30 random albums. 17 | 3. Run the `application.rb` file. This application will: 18 | 1. List all known singers and albums. 19 | 2. Select a random singer and update the name of this singer. 20 | 3. Select all singers whose last names start with an 'A'. 21 | 22 | Run the application with the command 23 | 24 | ```bash 25 | bundle exec rake run 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Runs a simple ActiveRecord tutorial application on a Spanner emulator." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[quickstart]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | 17 | 10.times do 18 | Singer.create first_name: first_names.sample, last_name: last_names.sample 19 | end 20 | 21 | 30.times do 22 | singer_id = Singer.all.sample.id 23 | Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/quickstart/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Read-only transactions 2 | 3 | In addition to locking read-write transactions, Cloud Spanner offers read-only transactions. 4 | Use a read-only transaction when you need to execute more than one read at the same timestamp. 5 | 6 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 7 | against that emulator. The emulator will automatically be stopped when the application finishes. 8 | 9 | Run the application with the command 10 | 11 | ```bash 12 | bundle exec rake run 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to execute a read-only transaction on Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[read-only-transactions]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | 17 | 5.times do 18 | Singer.create first_name: first_names.sample, last_name: last_names.sample 19 | end 20 | 21 | 20.times do 22 | singer_id = Singer.all.sample.id 23 | Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/read-only-transactions/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Read/Write transactions 2 | 3 | This example shows how to use read/write transactions using the Spanner ActiveRecord adapter. 4 | 5 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 6 | against that emulator. The emulator will automatically be stopped when the application finishes. 7 | 8 | Run the application with the command 9 | 10 | ```bash 11 | bundle exec rake run 12 | ``` 13 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to execute a read/write transaction on Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[read-write-transactions]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run 14 | from_album = nil 15 | to_album = nil 16 | # Use a read/write transaction to execute multiple statements as an atomic unit. 17 | ActiveRecord::Base.transaction do 18 | # Transfer a marketing budget of 10,000 from one album to another. 19 | from_album = Album.all.sample 20 | to_album = Album.where.not(id: from_album.id).sample 21 | 22 | puts "" 23 | puts "Transferring 10,000 marketing budget from #{from_album.title} (#{from_album.marketing_budget}) " \ 24 | "to #{to_album.title} (#{to_album.marketing_budget})" 25 | from_album.update marketing_budget: from_album.marketing_budget - 10000 26 | to_album.update marketing_budget: to_album.marketing_budget + 10000 27 | end 28 | puts "" 29 | puts "Budgets after update:" 30 | puts "Marketing budget #{from_album.title}: #{from_album.reload.marketing_budget}" 31 | puts "Marketing budget #{to_album.title}: #{to_album.reload.marketing_budget}" 32 | end 33 | end 34 | 35 | Application.run 36 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.numeric :marketing_budget 18 | t.references :singer, index: false, foreign_key: true 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | budgets = [15000, 25000, 10000, 20000, 30000, 12000, 13000] 17 | 18 | 5.times do 19 | Singer.create first_name: first_names.sample, last_name: last_names.sample 20 | end 21 | 22 | 20.times do 23 | singer_id = Singer.all.sample.id 24 | Album.create title: "#{adjectives.sample} #{nouns.sample}", marketing_budget: budgets.sample, singer_id: singer_id 25 | end 26 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/read-write-transactions/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Stale reads 2 | 3 | Read and query operations outside of a transaction block will by default be executed using a 4 | single-use read-only transaction with strong timestamp bound. This means that the read is 5 | guaranteed to return all data that has been committed at the time of the read. It is also possible 6 | to specify that the Spanner ActiveRecord provider should execute a stale read. This is done by 7 | specifying an optimizer hint for the read or query operation. The hints that are available are: 8 | 9 | * `max_staleness: ` 10 | * `exact_staleness: ` 11 | * `min_read_timestamp: ` 12 | * `read_timestamp: ` 13 | 14 | See https://cloud.google.com/spanner/docs/timestamp-bounds for more information on what the 15 | different timestamp bounds in Cloud Spanner mean. 16 | 17 | NOTE: These optimizer hints ONLY work OUTSIDE transactions. See the read-only-transactions 18 | samples for more information on how to specify a timestamp bound for a read-only transaction. 19 | 20 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 21 | against that emulator. The emulator will automatically be stopped when the application finishes. 22 | 23 | Run the application with the command 24 | 25 | ```bash 26 | bundle exec rake run 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to execute stale reads on Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[stale-reads]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | 17 | 5.times do 18 | Singer.create first_name: first_names.sample, last_name: last_names.sample 19 | end 20 | 21 | 20.times do 22 | singer_id = Singer.all.sample.id 23 | Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/stale-reads/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/tags/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Request tags and Transaction tags 2 | 3 | Queries can be annotated with request tags and transaction tags. These can be used to give 4 | you more insights in your queries and transactions. 5 | See https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags for more 6 | information about request and transaction tags in Cloud Spanner. 7 | 8 | You can use the [`annotate`](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-annotate) 9 | method in Ruby ActiveRecord to add a request and/or transaction tag to a __query__. You must 10 | prefix the annotation with either `request_tag:` or `transaction_tag:` to instruct the 11 | Cloud Spanner ActiveRecord provider to recognize the annotation as a tag. 12 | 13 | __NOTE:__ Ruby ActiveRecord does not add comments for `INSERT`, `UPDATE` and `DELETE` statements 14 | when you add annotations to a model. This means that these statements will not be tagged. 15 | 16 | Example: 17 | 18 | ```ruby 19 | 20 | ``` 21 | 22 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 23 | against that emulator. The emulator will automatically be stopped when the application finishes. 24 | 25 | Run the application with the command 26 | 27 | ```bash 28 | bundle exec rake run 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/snippets/tags/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to tag queries on Cloud Spanner with ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[tags]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/tags/application.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "io/console" 8 | require_relative "../config/environment" 9 | require_relative "models/singer" 10 | require_relative "models/album" 11 | 12 | class Application 13 | def self.run 14 | puts "" 15 | puts "Query all Albums and include a request tag" 16 | albums = Album.annotate("request_tag: query-all-albums").all 17 | puts "Queried #{albums.length} albums using a request tag" 18 | 19 | puts "" 20 | puts "Query all Albums in a transaction and include a request tag and a transaction tag" 21 | Album.transaction do 22 | albums = Album.annotate("request_tag: query-all-albums", "transaction_tag: sample-transaction").all 23 | puts "Queried #{albums.length} albums using a request and a transaction tag" 24 | end 25 | end 26 | end 27 | 28 | Application.run 29 | -------------------------------------------------------------------------------- /examples/snippets/tags/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/tags/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :singers do |t| 11 | t.string :first_name 12 | t.string :last_name 13 | end 14 | 15 | create_table :albums do |t| 16 | t.string :title 17 | t.references :singer, index: false, foreign_key: true 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/tags/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../../config/environment" 8 | require_relative "../models/singer" 9 | require_relative "../models/album" 10 | 11 | first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"] 12 | last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"] 13 | 14 | adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"] 15 | nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"] 16 | 17 | 5.times do 18 | Singer.create first_name: first_names.sample, last_name: last_names.sample 19 | end 20 | 21 | 20.times do 22 | singer_id = Singer.all.sample.id 23 | Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id 24 | end 25 | -------------------------------------------------------------------------------- /examples/snippets/tags/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Album < ActiveRecord::Base 8 | belongs_to :singer 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/tags/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Singer < ActiveRecord::Base 8 | has_many :albums 9 | end 10 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/README.md: -------------------------------------------------------------------------------- 1 | # Sample - Timestamp Data Type 2 | 3 | This example shows how to use the `TIMESTAMP` data type with the Spanner ActiveRecord adapter. A `TIMESTAMP` represents 4 | a specific point in time. Cloud Spanner always stores the timestamp in UTC, and it is not possible to include a 5 | specific timezone in a timestamp value in Cloud Spanner. Instead, a separate column containing a timezone can be used 6 | if your application needs to store both a timestamp and a timezone. This sample shows how to do this. 7 | 8 | ## Running the Sample 9 | 10 | The sample will automatically start a Spanner Emulator in a docker container and execute the sample 11 | against that emulator. The emulator will automatically be stopped when the application finishes. 12 | 13 | Run the application with the command 14 | 15 | ```bash 16 | bundle exec rake run 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "../config/environment" 8 | require "sinatra/activerecord/rake" 9 | 10 | desc "Sample showing how to work with the TIMESTAMP data type in ActiveRecord." 11 | task :run do 12 | Dir.chdir("..") { sh "bundle exec rake run[timestamp-data-type]" } 13 | end 14 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: spanner 3 | emulator_host: localhost:9010 4 | project: test-project 5 | instance: test-instance 6 | database: testdb 7 | pool: 5 8 | timeout: 5000 9 | schema_dump: false 10 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTables < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | create_table :meetings do |t| 11 | t.string :title 12 | # A `TIMESTAMP` column in Cloud Spanner contains a date/time value that designates a specific point in time. The 13 | # value is always stored in UTC. If you specify a date/time value in a different timezone, the value is 14 | # converted to UTC when saving it to the database. You can use a separate column to store the timezone of the 15 | # timestamp if that is vital for your application, and use that information when the timestamp is read back. 16 | t.datetime :meeting_time 17 | t.string :meeting_timezone 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | # 7 | -------------------------------------------------------------------------------- /examples/snippets/timestamp-data-type/models/meeting.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class Meeting < ActiveRecord::Base 8 | # Returns the meeting time in the local timezone. 9 | def local_meeting_time 10 | return unless meeting_time && Time.zone 11 | meeting_time.in_time_zone Time.zone 12 | end 13 | 14 | # Returns the time of the meeting in the timezone where the meeting is planned. 15 | def meeting_time_in_planned_zone 16 | return unless meeting_time && meeting_timezone 17 | meeting_time.in_time_zone meeting_timezone 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/spanner/column.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | # 7 | # frozen_string_literal: true 8 | 9 | module ActiveRecord 10 | module ConnectionAdapters 11 | module Spanner 12 | class Column < ConnectionAdapters::Column 13 | # rubocop:disable Style/OptionalBooleanParameter 14 | def initialize(name, default, sql_type_metadata = nil, null = true, 15 | default_function = nil, collation: nil, comment: nil, 16 | primary_key: false, **) 17 | # rubocop:enable Style/OptionalBooleanParameter 18 | super 19 | @primary_key = primary_key 20 | end 21 | 22 | def auto_incremented_by_db? 23 | sql_type_metadata.is_identity 24 | end 25 | 26 | def has_default? 27 | super && !virtual? 28 | end 29 | 30 | def virtual? 31 | sql_type_metadata.generated 32 | end 33 | 34 | def primary_key? 35 | @primary_key 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/spanner/schema_cache.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecord 8 | module ConnectionAdapters 9 | class SpannerSchemaCache 10 | def initialize conn 11 | @connection = conn 12 | @primary_and_parent_keys = {} 13 | end 14 | 15 | def primary_and_parent_keys table_name 16 | @primary_and_parent_keys[table_name] ||= 17 | @connection.primary_and_parent_keys table_name 18 | end 19 | 20 | def clear! 21 | @primary_and_parent_keys.clear 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_record/type/spanner/array.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecord 8 | module Type 9 | module Spanner 10 | class Array < Type::Value 11 | attr_reader :element_type 12 | 13 | delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :element_type 14 | 15 | # This constructor intentionally does not call super. 16 | # rubocop:disable Lint/MissingSuper 17 | def initialize element_type 18 | @element_type = element_type 19 | end 20 | # rubocop:enable Lint/MissingSuper 21 | 22 | def cast value 23 | return super if value.nil? 24 | return super unless value.respond_to? :map 25 | 26 | value.map do |v| 27 | @element_type.cast v 28 | end 29 | end 30 | 31 | def serialize value 32 | return super if value.nil? 33 | return super unless value.respond_to? :map 34 | 35 | if @element_type.is_a? ActiveRecord::Type::Decimal 36 | # Convert a decimal (NUMERIC) array to a String array to prevent it from being encoded as a FLOAT64 array. 37 | value.map do |v| 38 | next if v.nil? 39 | v.to_s 40 | end 41 | else 42 | value.map do |v| 43 | @element_type.serialize v 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/active_record/type/spanner/bytes.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | module ActiveRecord 10 | module Type 11 | module Spanner 12 | class Bytes < ActiveRecord::Type::Binary 13 | def deserialize value 14 | # Set this environment variable to disable de-serializing BYTES 15 | # to a StringIO instance. 16 | return super if ENV["SPANNER_BYTES_DESERIALIZE_DISABLED"] 17 | 18 | return super value if value.nil? 19 | return StringIO.new Base64.strict_decode64(value) if value.is_a? ::String 20 | value 21 | end 22 | 23 | def serialize value 24 | return super value if value.nil? 25 | 26 | if value.respond_to?(:read) && value.respond_to?(:rewind) 27 | value.rewind 28 | value = value.read 29 | end 30 | 31 | Base64.strict_encode64 value.force_encoding("ASCII-8BIT") 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/active_record/type/spanner/time.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | module ActiveRecord 10 | module Type 11 | module Spanner 12 | class Time < ActiveRecord::Type::Time 13 | def serialize_with_isolation_level value, isolation_level 14 | if value == :commit_timestamp 15 | return "PENDING_COMMIT_TIMESTAMP()" if isolation_level == :dml 16 | return "spanner.commit_timestamp()" if isolation_level == :mutation 17 | end 18 | 19 | serialize value 20 | end 21 | 22 | def serialize value 23 | val = super value 24 | val.acts_like?(:time) ? val.utc.rfc3339(9) : val 25 | end 26 | 27 | def user_input_in_time_zone value 28 | return value.in_time_zone if value.is_a? ::Time 29 | super value 30 | end 31 | 32 | private 33 | 34 | def cast_value value 35 | if value.is_a? ::String 36 | value = value.empty? ? nil : ::Time.parse(value) 37 | end 38 | 39 | value 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/activerecord-spanner-adapter.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "activerecord_spanner_adapter/version" 8 | 9 | if defined?(Rails) 10 | module ActiveRecord 11 | module ConnectionAdapters 12 | class SpannerRailtie < ::Rails::Railtie 13 | rake_tasks do 14 | require "active_record/tasks/spanner_database_tasks" 15 | end 16 | 17 | ActiveSupport.on_load :active_record do 18 | if Rails.version >= "7.2.0" 19 | ActiveRecord::ConnectionAdapters.register("spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter") 20 | else 21 | require "active_record/connection_adapters/spanner_adapter" 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/errors.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecordSpannerAdapter 8 | class Error < StandardError 9 | end 10 | 11 | class NotSupportedError < Error 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/foreign_key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecordSpannerAdapter 8 | class ForeignKey 9 | attr_accessor :table_schema 10 | attr_accessor :table_name 11 | attr_accessor :name 12 | attr_accessor :columns 13 | attr_accessor :ref_schema 14 | attr_accessor :ref_table 15 | attr_accessor :ref_columns 16 | attr_accessor :on_delete 17 | attr_accessor :on_update 18 | 19 | def initialize \ 20 | table_name, 21 | name, 22 | columns, 23 | ref_table, 24 | ref_columns, 25 | on_delete: nil, 26 | on_update: nil, 27 | table_schema: "", 28 | ref_schema: "" 29 | @table_schema = table_schema 30 | @table_name = table_name 31 | @name = name 32 | @columns = Array(columns) 33 | @ref_schema = ref_schema 34 | @ref_table = ref_table 35 | @ref_columns = Array(ref_columns) 36 | @on_delete = on_delete unless on_delete == "NO ACTION" 37 | @on_update = on_update unless on_update == "NO ACTION" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/index/column.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecordSpannerAdapter 8 | class Index 9 | class Column 10 | attr_accessor :table_name 11 | attr_accessor :schema_name 12 | attr_accessor :index_name 13 | attr_accessor :name 14 | attr_accessor :order 15 | attr_accessor :ordinal_position 16 | 17 | def initialize \ 18 | table_name, 19 | index_name, 20 | name, 21 | schema_name: "", 22 | order: nil, 23 | ordinal_position: nil 24 | @table_name = table_name.to_s 25 | @index_name = index_name.to_s 26 | @schema_name = schema_name.to_s 27 | @name = name.to_s 28 | @order = order.to_s.upcase if order 29 | @ordinal_position = ordinal_position 30 | end 31 | 32 | def storing? 33 | @ordinal_position.nil? 34 | end 35 | 36 | def desc? 37 | @order == "DESC" 38 | end 39 | 40 | def desc! 41 | @order = "DESC" 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/primary_key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecord 8 | module AttributeMethods 9 | module PrimaryKey 10 | module ClassMethods 11 | def primary_and_parent_key 12 | reset_primary_and_parent_key unless defined? @primary_and_parent_key 13 | @primary_and_parent_key 14 | end 15 | 16 | def reset_primary_and_parent_key 17 | self.primary_and_parent_key = base_class? ? fetch_primary_and_parent_key : base_class.primary_and_parent_key 18 | end 19 | 20 | def fetch_primary_and_parent_key 21 | connection.spanner_schema_cache.primary_and_parent_keys table_name \ 22 | if self != ActiveRecord::Base && table_exists? 23 | end 24 | 25 | def primary_and_parent_key= value 26 | @primary_and_parent_key = value 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/relation.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecord 8 | module CpkExtension 9 | def cpk_subquery stmt 10 | return super unless spanner_adapter? 11 | # The composite_primary_key gem will by default generate WHERE clauses using an IN clause with a multi-column 12 | # sub select, e.g.: SELECT * FROM my_table WHERE (id1, id2) IN (SELECT id1, id2 FROM my_table WHERE ...). 13 | # This is not supported in Cloud Spanner. Instead, composite_primary_key should generate an EXISTS clause. 14 | cpk_exists_subquery stmt 15 | end 16 | end 17 | 18 | class Relation 19 | prepend CpkExtension 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/activerecord_spanner_adapter/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module ActiveRecordSpannerAdapter 8 | VERSION = "2.3.0".freeze 9 | end 10 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bump-minor-pre-major": true, 3 | "bump-patch-for-minor-pre-major": false, 4 | "draft": false, 5 | "include-component-in-tag": true, 6 | "include-v-in-tag": true, 7 | "prerelease": false, 8 | "release-type": "ruby-yoshi", 9 | "skip-github-release": false, 10 | "separate-pull-requests": true, 11 | "tag-separator": "/", 12 | "sequential-calls": true, 13 | "packages": { 14 | ".": { 15 | "component": "activerecord-spanner-adapter", 16 | "version-file": "lib/activerecord_spanner_adapter/version.rb" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/activerecord_spanner_adapter/foreign_key_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "test_helper" 8 | 9 | class InformationSchemaForeignKeyTest < TestHelper::MockActiveRecordTest 10 | attr_reader :table_name, :column_name, :contraint_name, 11 | :ref_table_name, :ref_column_name 12 | 13 | def setup 14 | @table_name = "test-table" 15 | @column_name = "test-column" 16 | @contraint_name = "test-contraint" 17 | @ref_table_name = "test-ref-table" 18 | @ref_column_name = "test-ref-column" 19 | end 20 | 21 | def test_create_instance_of_foreign_key 22 | fk = ActiveRecordSpannerAdapter::ForeignKey.new( 23 | table_name, contraint_name, column_name, 24 | ref_table_name, ref_column_name 25 | ) 26 | 27 | assert_equal fk.table_name, table_name 28 | assert_equal fk.columns, [column_name] 29 | assert_equal fk.name, contraint_name 30 | assert_equal fk.ref_table, ref_table_name 31 | assert_equal fk.ref_columns, [ref_column_name] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/activerecord_spanner_adapter/index/column_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "test_helper" 8 | 9 | class InformationSchemaIndexColumnTest < TestHelper::MockActiveRecordTest 10 | attr_reader :table_name, :column_name, :index_name 11 | 12 | def setup 13 | super 14 | @table_name = "test-table" 15 | @column_name = "test-column" 16 | @index_name = "index-name" 17 | end 18 | 19 | def test_create_index_column_instance 20 | column = ActiveRecordSpannerAdapter::Index::Column.new( 21 | table_name, index_name, column_name, 22 | order: "DESC", ordinal_position: 1 23 | ) 24 | 25 | assert_equal column.name, column_name 26 | assert_equal column.table_name, table_name 27 | assert_equal column.index_name, index_name 28 | assert_equal column.order, "DESC" 29 | assert_equal column.desc?, true 30 | assert_equal column.ordinal_position, 1 31 | assert_equal column.storing?, false 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/activerecord_spanner_adapter/index_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "test_helper" 8 | 9 | class InformationSchemaIndexTest < TestHelper::MockActiveRecordTest 10 | attr_reader :table_name, :parent_table_name, :index_name 11 | 12 | def setup 13 | super 14 | @table_name = "test-table" 15 | @parent_table_name = "test-parent-table" 16 | @index_name = "test-index" 17 | end 18 | 19 | 20 | def test_create_instance_of_index 21 | column1 = new_index_column( 22 | table_name: table_name, index_name: index_name, column_name: "col1", 23 | order: "DESC", ordinal_position: 1 24 | ) 25 | column2 = new_index_column( 26 | table_name: table_name, index_name: index_name, column_name: "col2", 27 | ordinal_position: 0 28 | ) 29 | 30 | index = ActiveRecordSpannerAdapter::Index.new( 31 | table_name, index_name, [column1, column2], 32 | unique: true, storing: ["col1"] 33 | ) 34 | 35 | assert_equal index.table, table_name 36 | assert_equal index.name, index_name 37 | assert_equal index.columns, [column1, column2] 38 | assert_equal index.unique, true 39 | assert_equal index.storing, ["col1"] 40 | assert_equal index.primary?, false 41 | assert_equal index.columns_by_position, [column2, column1] 42 | assert_equal index.orders, ({ "col1" => :desc, "col2" => :asc}) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/activerecord_spanner_adapter/table_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "test_helper" 8 | 9 | class InformationSchemaTableTest < TestHelper::MockActiveRecordTest 10 | attr_reader :table_name, :parent_table_name 11 | 12 | def setup 13 | super 14 | @table_name = "test-table" 15 | @parent_table_name = "test-parent-table" 16 | end 17 | 18 | def test_create_a_instance_of_table 19 | column1 = new_table_column( 20 | table_name: table_name, column_name: "id", type: "STRING", limit: 36 21 | ) 22 | column1.primary_key = true 23 | column2 = new_table_column( 24 | table_name: table_name, column_name: "DESC", type: "STRING", limit: "MAX" 25 | ) 26 | 27 | table = ActiveRecordSpannerAdapter::Table.new( 28 | table_name, 29 | parent_table: parent_table_name, 30 | on_delete: "CASCADE", 31 | schema_name: "", 32 | catalog: "" 33 | ) 34 | 35 | table.columns = [column1, column2] 36 | 37 | assert_equal table.name, table_name 38 | assert_equal table.parent_table, parent_table_name 39 | assert_equal table.on_delete, "CASCADE" 40 | assert_equal table.cascade?, true 41 | assert_empty table.catalog 42 | assert_empty table.schema_name 43 | assert_equal table.columns.length, 2 44 | assert_equal table.primary_keys, ["id"] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/activerecord_spanner_adapter_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "test_helper" 8 | 9 | describe ActiveRecordSpannerAdapter do 10 | describe "#version" do 11 | it "check gem has a version number" do 12 | _(ActiveRecordSpannerAdapter::VERSION).wont_be_nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | module TestInterleavedTables 10 | class Album < ActiveRecord::Base 11 | self.primary_keys = :singerid, :albumid 12 | 13 | belongs_to :singer, foreign_key: :singerid 14 | 15 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is (`singerid`, `albumid`). 16 | has_many :tracks, foreign_key: [:singerid, :albumid] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestInterleavedTables 8 | class Singer < ActiveRecord::Base 9 | has_many :albums, foreign_key: :singerid 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | module TestInterleavedTables 10 | class Track < ActiveRecord::Base 11 | self.primary_keys = :singerid, :albumid, :trackid 12 | 13 | belongs_to :album, foreign_key: [:singerid, :albumid] 14 | belongs_to :singer, foreign_key: :singerid 15 | 16 | def album=value 17 | super 18 | # Ensure the singer of this track is equal to the singer of the album that is set. 19 | self.singer = value&.singer 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table_version_7_1_and_higher/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestInterleavedTables_7_1_AndHigher 8 | class Album < ActiveRecord::Base 9 | # self.primary_keys = :singerid, :albumid 10 | 11 | belongs_to :singer, foreign_key: :singerid 12 | 13 | # `tracks` is defined as INTERLEAVE IN PARENT `albums`. 14 | # The primary key of `albums` is (`singerid`, `albumid`). 15 | if ActiveRecord::VERSION::MAJOR >= 8 16 | has_many :tracks, foreign_key: [:singerid, :albumid] 17 | else 18 | has_many :tracks, query_constraints: [:singerid, :albumid] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table_version_7_1_and_higher/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestInterleavedTables_7_1_AndHigher 8 | class Singer < ActiveRecord::Base 9 | has_many :albums, foreign_key: :singerid 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/activerecord_spanner_interleaved_table_version_7_1_and_higher/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestInterleavedTables_7_1_AndHigher 8 | class Track < ActiveRecord::Base 9 | # self.primary_keys = :singerid, :albumid, :trackid 10 | 11 | if ActiveRecord::VERSION::MAJOR >= 8 12 | belongs_to :album, foreign_key: [:singerid, :albumid] 13 | else 14 | belongs_to :album, query_constraints: [:singerid, :albumid] 15 | end 16 | belongs_to :singer, foreign_key: :singerid 17 | 18 | def album=value 19 | super 20 | # Ensure the singer of this track is equal to the singer of the album that is set. 21 | self.singer = value&.singer 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/cloudspannerlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/ruby-spanner-activerecord/0a1020ac60ff9ddaae1634a2178a71bc0de9480c/test/activerecord_spanner_mock_server/cloudspannerlogo.png -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class Album < ActiveRecord::Base 9 | belongs_to :singer 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/all_types.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class AllTypes < ActiveRecord::Base 8 | end 9 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/binary_project.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require_relative "string_io" 10 | 11 | class BinaryProject < ActiveRecord::Base 12 | belongs_to :owner, class_name: 'User' 13 | 14 | before_create :set_uuid 15 | private 16 | 17 | def set_uuid 18 | self.id ||= StringIO.new(SecureRandom.random_bytes(16)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/other_adapter.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class System < ActiveRecord::Base 9 | has_many :projects 10 | end 11 | 12 | class Plan < ActiveRecord::Base 13 | has_many :projects 14 | end 15 | 16 | class Project < ActiveRecord::Base 17 | belongs_to :system 18 | belongs_to :plan 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class Singer < ActiveRecord::Base 9 | has_many :albums 10 | attr_reader :full_name 11 | 12 | def initialize(attributes = nil) 13 | super attributes 14 | @full_name = "" 15 | end 16 | 17 | after_save do 18 | @full_name = first_name + ' ' + last_name 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/string_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StringIO 4 | def ==(o) 5 | o.class == self.class && self.to_base64 == o.to_base64 6 | end 7 | 8 | def eql?(o) 9 | self == o 10 | end 11 | 12 | def hash 13 | to_base64.hash 14 | end 15 | 16 | def to_base64 17 | self.rewind 18 | value = self.read 19 | Base64.strict_encode64 value.force_encoding("ASCII-8BIT") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/table_with_commit_timestamp.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class TableWithCommitTimestamp < ActiveRecord::Base 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/table_with_identity.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class TableWithIdentity < ActiveRecord::Base 9 | self.table_name = :table_with_identity 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/table_with_sequence.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class TableWithSequence < ActiveRecord::Base 9 | self.table_name = :table_with_sequence 10 | self.sequence_name = :test_sequence 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/user.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | # frozen_string_literal: true 8 | 9 | require_relative "string_io" 10 | 11 | class User < ActiveRecord::Base 12 | has_many :binary_projects, foreign_key: :owner_id 13 | 14 | before_create :set_uuid 15 | private 16 | 17 | def set_uuid 18 | self.id ||= StringIO.new(SecureRandom.random_bytes(16)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/activerecord_spanner_mock_server/models/versioned_singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class VersionedSinger < ActiveRecord::Base 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/01_create_singer_and_album_tables.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateSingerAndAlbumTables < ActiveRecord::Migration[6.0] 8 | def change 9 | # Record the current primary key prefix type to reset it after running this change. 10 | current_prefix_type = ActiveRecord::Base.primary_key_prefix_type 11 | begin 12 | ActiveRecord::Base.primary_key_prefix_type = :table_name 13 | # Start a DDL batch that will be used for the entire change. 14 | connection.start_batch_ddl 15 | create_table :singers do |t| 16 | t.column :first_name, :string, limit: 200 17 | t.string :last_name 18 | end 19 | 20 | create_table :albums do |t| 21 | t.string :title 22 | t.integer :singer_id 23 | end 24 | 25 | add_foreign_key :albums, :singers 26 | 27 | add_column :singers, "place_of_birth", "STRING(MAX)" 28 | 29 | create_join_table :singers, :albums 30 | 31 | # Execute the change as one DDL batch. 32 | connection.run_batch 33 | rescue StandardError 34 | connection.abort_batch 35 | raise 36 | ensure 37 | ActiveRecord::Base.primary_key_prefix_type = current_prefix_type 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/02_create_tables_without_batching.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTablesWithoutBatching < ActiveRecord::Migration[6.0] 8 | def change 9 | # Create two tables without using DDL batching. 10 | create_table :table1 do |t| 11 | t.string :col1 12 | t.string :col2 13 | end 14 | 15 | create_table :table2 do |t| 16 | t.string :col1 17 | t.string :col2 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/05_create_table_with_commit_timestamp.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTableWithCommitTimestamp < ActiveRecord::Migration[6.0] 8 | def change 9 | create_table :table1 do |t| 10 | t.string :value 11 | t.datetime :last_updated, allow_commit_timestamp: true 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/06_create_table_with_generated_column.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTableWithGeneratedColumn < ActiveRecord::Migration[6.0] 8 | def change 9 | create_table :singers do |t| 10 | t.string :first_name, limit: 100 11 | t.string :last_name, limit: 200 12 | t.string :full_name, limit: 300, as: "COALESCE(first_name || ' ', '') || last_name", stored: true 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/07_create_interleaved_index.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateInterleavedIndex < ActiveRecord::Migration[6.0] 8 | def change 9 | # Start a DDL batch that will be used for the entire change. 10 | connection.ddl_batch do 11 | add_index :albums, [:singerid, :title], interleave_in: :singers 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/08_create_null_filtered_index.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateNullFilteredIndex < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | add_index :singers, :picture, null_filtered: true 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/09_create_index_storing.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateIndexStoring < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.ddl_batch do 10 | add_index :singers, :full_name, storing: [:first_name, :last_name] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/10_create_parent_and_child_with_uuid_pk.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | class CreateParentAndChildWithUuidPk < ActiveRecord::Migration[6.0] 10 | def change 11 | # Execute the entire migration as one DDL batch. 12 | connection.ddl_batch do 13 | create_table :parent_with_uuid_pk, id: false do |t| 14 | t.primary_key :parentid, :string, limit: 36 15 | t.string :first_name 16 | t.string :last_name 17 | end 18 | 19 | create_table :child_with_uuid_pk, id: false do |t| 20 | t.interleave_in :parent_with_uuid_pk 21 | t.parent_key :parentid, type: 'STRING(36)' 22 | t.primary_key :childid 23 | t.string :title 24 | end 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/11_create_table_with_default_value.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateTableWithDefaultValue < ActiveRecord::Migration[6.0] 8 | def change 9 | create_table :singers do |t| 10 | t.string :name, null: false, default: "no name" 11 | t.integer :age, null: false, default: 0 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/12_create_bit_reversed_sequence.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateBitReversedSequence < ActiveRecord::Migration[6.0] 8 | def change 9 | connection.start_batch_ddl 10 | connection.execute "create sequence test_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')" 11 | 12 | create_table :table_with_sequence, id: false do |t| 13 | t.integer :id, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE test_sequence)" } 14 | t.string :name, null: false 15 | t.integer :age, null: false 16 | end 17 | connection.run_batch 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/db/migrate/13_create_auto_generated_primary_key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | class CreateAutoGeneratedPrimaryKey < ActiveRecord::Migration[7.0] 8 | def change 9 | connection.start_batch_ddl 10 | create_table :table_with_identity do |t| 11 | t.string :name, null: false 12 | t.integer :age, null: false 13 | end 14 | connection.run_batch 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/album.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestMigrationsWithMockServer 8 | class Album < ActiveRecord::Base 9 | belongs_to :singer 10 | has_many :tracks 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/child_with_uuid_pk.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | module TestMigrationsWithMockServer 10 | class ChildWithUuidPk < ActiveRecord::Base 11 | # Register both primary key columns with composite_primary_keys 12 | self.primary_keys = :parentid, :childid 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/parent_with_uuid_pk.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require "composite_primary_keys" 8 | 9 | module TestMigrationsWithMockServer 10 | class ParentWithUuidPk < ActiveRecord::Base 11 | self.primary_keys = :parentid 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/singer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestMigrationsWithMockServer 8 | class Singer < ActiveRecord::Base 9 | has_many :albums 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/table_with_identity.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class TableWithIdentity < ActiveRecord::Base 9 | self.table_name = :table_with_identity 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/table_with_sequence.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module MockServerTests 8 | class TableWithSequence < ActiveRecord::Base 9 | self.table_name = :table_with_sequence 10 | self.sequence_name = :test_sequence 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/migrations_with_mock_server/models/track.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | module TestMigrationsWithMockServer 8 | class Track < ActiveRecord::Base 9 | belongs_to :album 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/mock_server/database_admin_mock_server.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Use of this source code is governed by an MIT-style 4 | # license that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require_relative "statement_result" 8 | require "grpc" 9 | require "gapic/grpc/service_stub" 10 | require "securerandom" 11 | 12 | require "google/spanner/admin/database/v1/spanner_database_admin_pb" 13 | require "google/spanner/admin/database/v1/spanner_database_admin_services_pb" 14 | require "google/cloud/spanner/admin/database/v1/database_admin" 15 | require "google/longrunning/operations_pb" 16 | 17 | # Mock implementation of Spanner Database Admin 18 | class DatabaseAdminMockServer < Google::Cloud::Spanner::Admin::Database::V1::DatabaseAdmin::Service 19 | attr_reader :requests 20 | 21 | def initialize 22 | super 23 | @requests = [] 24 | end 25 | 26 | def get_database request, _unused_call 27 | @requests << request 28 | Google::Cloud::Spanner::Admin::Database::V1::Database.new name: request.name 29 | end 30 | 31 | def update_database_ddl request, _unused_call 32 | @requests << request 33 | Google::Longrunning::Operation.new( 34 | done: true, 35 | name: "#{request.database}/operations/#{SecureRandom.uuid}", 36 | ) 37 | end 38 | 39 | end 40 | --------------------------------------------------------------------------------