From f58d196036e4ca336faee911c06e6d5364396f1e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:32:10 +0000 Subject: [PATCH 1/2] Fix MySQL :copyfrom into mixed-case tables; reject unknown isolation: with ArgumentError; document Steep timeout, Struct member names and the SQLite slice limit Co-Authored-By: Doron Eli Rachman --- README.md | 14 +- docs/Coverage.md | 3 +- examples/authors/mysql/query.sql | 13 + examples/authors/mysql/schema.sql | 2 + examples/authors/postgresql/query.sql | 9 + examples/authors/postgresql/schema.sql | 2 + examples/authors/sqlite/query.sql | 6 + examples/authors/sqlite/schema.sql | 2 + examples/mysql2/models.rb | 3 + examples/mysql2/models.rbs | 6 + examples/mysql2/query_sql.rb | 62 +++++ examples/mysql2/query_sql.rbs | 30 +++ examples/pg/models.rb | 5 + examples/pg/models.rbs | 6 + examples/pg/query_sql.rb | 46 ++++ examples/pg/query_sql.rbs | 22 ++ examples/sqlite3/models.rb | 5 + examples/sqlite3/models.rbs | 6 + examples/sqlite3/query_sql.rb | 38 +++ examples/sqlite3/query_sql.rbs | 18 ++ lib/sqlc_gen_ruby/code.rb | 2 +- lib/sqlc_gen_ruby/drivers/base.rb | 6 + lib/sqlc_gen_ruby/drivers/mysql2.rb | 21 ++ lib/sqlc_gen_ruby/drivers/pg.rb | 2 + lib/sqlc_gen_ruby/method_gen.rb | 6 +- steep_expectations.yml | 246 +++++++++--------- test/fixtures/mysql2.bin | 19 +- test/fixtures/pg.bin | 18 +- test/fixtures/sqlite3.bin | Bin 18758 -> 19376 bytes test/golden/mysql2/default/models.rb | 3 + test/golden/mysql2/default/models.rbs | 6 + test/golden/mysql2/default/my_queries_sql.rb | 2 + test/golden/mysql2/default/other_sql.rb | 2 + test/golden/mysql2/default/query_sql.rb | 62 +++++ test/golden/mysql2/default/query_sql.rbs | 30 +++ test/golden/mysql2/ruby31_gemfile/models.rb | 2 + test/golden/mysql2/ruby31_gemfile/models.rbs | 7 + .../mysql2/ruby31_gemfile/my_queries_sql.rb | 2 + .../golden/mysql2/ruby31_gemfile/other_sql.rb | 2 + .../golden/mysql2/ruby31_gemfile/query_sql.rb | 59 +++++ .../mysql2/ruby31_gemfile/query_sql.rbs | 33 +++ test/golden/mysql2/ruby32_notypes/models.rb | 3 + .../mysql2/ruby32_notypes/my_queries_sql.rb | 2 + .../golden/mysql2/ruby32_notypes/other_sql.rb | 2 + .../golden/mysql2/ruby32_notypes/query_sql.rb | 62 +++++ test/golden/pg/default/models.rb | 5 + test/golden/pg/default/models.rbs | 6 + test/golden/pg/default/my_queries_sql.rb | 2 + test/golden/pg/default/other_sql.rb | 2 + test/golden/pg/default/query_sql.rb | 46 ++++ test/golden/pg/default/query_sql.rbs | 22 ++ test/golden/pg/ruby31_gemfile/models.rb | 4 + test/golden/pg/ruby31_gemfile/models.rbs | 7 + .../pg/ruby31_gemfile/my_queries_sql.rb | 2 + test/golden/pg/ruby31_gemfile/other_sql.rb | 2 + test/golden/pg/ruby31_gemfile/query_sql.rb | 44 ++++ test/golden/pg/ruby31_gemfile/query_sql.rbs | 24 ++ test/golden/pg/ruby32_notypes/models.rb | 5 + .../pg/ruby32_notypes/my_queries_sql.rb | 2 + test/golden/pg/ruby32_notypes/other_sql.rb | 2 + test/golden/pg/ruby32_notypes/query_sql.rb | 46 ++++ test/golden/sqlite3/default/models.rb | 5 + test/golden/sqlite3/default/models.rbs | 6 + test/golden/sqlite3/default/query_sql.rb | 38 +++ test/golden/sqlite3/default/query_sql.rbs | 18 ++ test/golden/sqlite3/ruby31_gemfile/models.rb | 4 + test/golden/sqlite3/ruby31_gemfile/models.rbs | 7 + .../sqlite3/ruby31_gemfile/query_sql.rb | 36 +++ .../sqlite3/ruby31_gemfile/query_sql.rbs | 20 ++ test/golden/sqlite3/ruby32_notypes/models.rb | 5 + .../sqlite3/ruby32_notypes/query_sql.rb | 38 +++ tests/end2end_mysql2.rb | 10 +- tests/end2end_pg.rb | 8 +- tests/end2end_sqlite3.rb | 5 + 74 files changed, 1183 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index 49e4d1a..5ba6659 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ end transaction is over. Other query objects (the parent `queries`, another generated class sharing the pool) used from another fiber or thread check out their own connection and run outside the transaction: pass `tx` instead. * `isolation:` (`pg`, `mysql2`) takes `:read_uncommitted`, `:read_committed`, `:repeatable_read`, `:serializable`; - `mode:` (`sqlite3`) takes `:deferred`, `:immediate`, `:exclusive`. Other values raise before anything runs, and so + `mode:` (`sqlite3`) takes `:deferred`, `:immediate`, `:exclusive`. Other values raise `ArgumentError` before anything runs, and so do `isolation:` / `mode:` on a nested `transaction` (a savepoint cannot change them). * PostgreSQL: if a statement fails and the block rescues the error, PostgreSQL still rolls the whole transaction back, so `transaction` raises `PG::InFailedSqlTransaction` instead of pretending it committed. In a nested @@ -166,6 +166,8 @@ server; duplicate keys and invalid values raise `Mysql2::Error` instead of being `INSERT OR REPLACE` / `OR IGNORE` / `ON CONFLICT ... DO ...` on SQLite, `INSERT IGNORE` (duplicates skipped) and `REPLACE` on MySQL. Clauses `COPY` / `LOAD DATA` cannot express (`ON CONFLICT` / `RETURNING` on PostgreSQL, `ON DUPLICATE KEY UPDATE` on MySQL) are generation errors; use `:batchexec` for those. +* MySQL: `LOAD DATA` names the table and columns as the `INSERT` writes them (`` `MixedCase` ``): sqlc reports MySQL + names in lowercase, but table names are case-sensitive on Linux. * MySQL: `LOAD DATA` commits an open transaction that was started with raw SQL (`BEGIN`); call `:copyfrom` inside `transaction` instead. @@ -195,14 +197,17 @@ server; duplicate keys and invalid values raise `Mysql2::Error` instead of being * SQLite follows its [type affinity rules](https://www.sqlite.org/datatype3.html#determination_of_column_affinity) (`VARCHAR(255)` → `String`, `DECIMAL(10,5)` → `Float`, `UNSIGNED BIG INT` → `Integer`); `JSON` and `UUID` columns are `String`s, also when SQLite stored numeric-looking text as a number (`'42'` → `"42"`, in SQLite's spelling: `'1.50'` → `"1.5"`). - The generated SQLite code uses `Monitor`: if you run Steep, add `library "monitor"` to your `Steepfile`. + The generated SQLite code uses `Monitor`: if you run Steep, add `library "monitor"` to your `Steepfile`. Code + generated for a `rubyVersion` below 3.3 also uses `Timeout` (see [Transactions](#transactions)): add `library "timeout"` too. ### Names * Query methods and keyword arguments are snake_cased, with acronyms kept together (`MIN_ID` → `min_id`, `HTTPServer` → `http_server`); Ruby keywords get a trailing `_` (`end_:`). Row members use the column spelling reported by sqlc for the engine, except names that conflict with row methods, including the private ones `Data` / `Struct` call while building and copying rows (`class_`, `hash_`, `initialize_`): PostgreSQL and SQLite keep `"camelCase"` as `:camelCase`, while MySQL reports it as `:camelcase`. A column named - `end` is the row member `:end` but the argument `end_:`. + `end` is the row member `:end` but the argument `end_:`. `Struct` rows (`rubyVersion` below 3.2) have more methods + than `Data` rows, so more names get the `_`: a column `count` is `:count` from 3.2 but `:count_` below + (`Struct#count` would otherwise be `Enumerable#count`); check the generated `.rbs` when changing `rubyVersion`. * A parameter used twice is one keyword when named with `sqlc.arg`; repeated `?` / `$n` of one column become `id:`, `id_2:`. * Models may be named like Ruby or driver constants (tables `exceptions`, `data`, `threads`, `sets`, ...): the generated code and `.rbs` refer to those as `::Exception`, `::Data`, `::Thread`, ... so the models cannot shadow them. @@ -212,6 +217,9 @@ server; duplicate keys and invalid values raise `Mysql2::Error` instead of being ### Limitations * Two result columns with the same name (e.g. `a.id, b.id`) are an error: alias one (`b.id AS book_id`) or use `sqlc.embed`. * SQLite's `NUMERIC` affinity can return an `Integer` where `Float` is declared. +* SQLite: a statement takes at most 32,766 parameters (`SQLITE_MAX_VARIABLE_NUMBER`), so a `sqlc.slice` with more + elements raises `SQLite3::SQLException: too many SQL variables`; on MySQL the limit is the server's + `max_allowed_packet`. Load larger sets into a table (`:copyfrom`) and join it instead. * `:batchexec` / `:batchone` / `:batchmany` are not atomic on their own: wrap them in `transaction` for all-or-nothing. * SQLite: sqlc does not see parameters in `ON CONFLICT ... DO UPDATE SET` (it leaves `sqlc.arg(...)` in the SQL or reports fewer parameters than there are `?`), so such queries are generation errors. Use `excluded.` diff --git a/docs/Coverage.md b/docs/Coverage.md index 04afe42..4e282da 100644 --- a/docs/Coverage.md +++ b/docs/Coverage.md @@ -24,7 +24,7 @@ only when the behavior is rejected or decided during generation and generated co | Names: keywords, method collisions (also private row hooks such as `initialize`), acronyms, quoted mixed case, repeated `$n`, plural models | [`test_keyword_and_method_names_keep_distinct_row_and_argument_names`](../tests/end2end_pg.rb#test_keyword_and_method_names_keep_distinct_row_and_argument_names), [`test_repeated_column_parameters_get_distinct_names`](../tests/end2end_pg.rb#test_repeated_column_parameters_get_distinct_names) | [`test_keyword_and_method_names_keep_distinct_row_and_argument_names`](../tests/end2end_mysql2.rb#test_keyword_and_method_names_keep_distinct_row_and_argument_names), [`test_repeated_positional_parameters_get_distinct_names`](../tests/end2end_mysql2.rb#test_repeated_positional_parameters_get_distinct_names) | [`test_keyword_and_method_names_keep_distinct_row_and_argument_names`](../tests/end2end_sqlite3.rb#test_keyword_and_method_names_keep_distinct_row_and_argument_names) | [`test_snake_case_keeps_acronyms_together`](../test/features_test.rb#test_snake_case_keeps_acronyms_together), [`test_query_names_cannot_replace_generated_methods`](../test/features_test.rb#test_query_names_cannot_replace_generated_methods), [`test_tables_with_the_same_singular_get_distinct_models`](../test/features_test.rb#test_tables_with_the_same_singular_get_distinct_models), [`test_columns_named_like_row_methods_still_build_and_read_rows`](../test/features_test.rb#test_columns_named_like_row_methods_still_build_and_read_rows) | | Connections, injected pools, reconnect behavior and SQLite serialization | [`test_queries_can_share_an_injected_pool`](../tests/end2end_pg.rb#test_queries_can_share_an_injected_pool), [`test_a_connection_closed_by_the_server_is_reconnected`](../tests/end2end_pg.rb#test_a_connection_closed_by_the_server_is_reconnected), [`test_connection_lost_inside_transaction_is_not_reconnected_until_the_transaction_ends`](../tests/end2end_pg.rb#test_connection_lost_inside_transaction_is_not_reconnected_until_the_transaction_ends) | [`test_queries_can_share_an_injected_pool`](../tests/end2end_mysql2.rb#test_queries_can_share_an_injected_pool), [`test_a_connection_closed_by_the_server_is_replaced`](../tests/end2end_mysql2.rb#test_a_connection_closed_by_the_server_is_replaced), [`test_a_stopped_async_task_does_not_leave_a_closed_connection_in_the_pool`](../tests/end2end_mysql2.rb#test_a_stopped_async_task_does_not_leave_a_closed_connection_in_the_pool) | [`test_fibers_sharing_a_database_take_turns`](../tests/end2end_sqlite3.rb#test_fibers_sharing_a_database_take_turns), [`test_threads_sharing_a_database_take_turns_in_transactions`](../tests/end2end_sqlite3.rb#test_threads_sharing_a_database_take_turns_in_transactions), [`test_a_database_returning_hash_rows_still_builds_rows`](../tests/end2end_sqlite3.rb#test_a_database_returning_hash_rows_still_builds_rows) | [`test_engine_must_match_the_driver`](../test/features_test.rb#test_engine_must_match_the_driver) | | Transaction result, commit/rollback, nested savepoint, exit semantics and rescued PostgreSQL error | [`test_nested_savepoint_rolls_back_while_outer_transaction_commits`](../tests/end2end_pg.rb#test_nested_savepoint_rolls_back_while_outer_transaction_commits), [`test_transaction_nonlocal_exit_semantics_follow_timeout_version`](../tests/end2end_pg.rb#test_transaction_nonlocal_exit_semantics_follow_timeout_version), [`test_transaction_whose_failed_statement_was_rescued_raises_instead_of_committing`](../tests/end2end_pg.rb#test_transaction_whose_failed_statement_was_rescued_raises_instead_of_committing) | [`test_nested_savepoint_rolls_back_while_outer_transaction_commits`](../tests/end2end_mysql2.rb#test_nested_savepoint_rolls_back_while_outer_transaction_commits), [`test_transaction_nonlocal_exit_semantics_follow_timeout_version`](../tests/end2end_mysql2.rb#test_transaction_nonlocal_exit_semantics_follow_timeout_version) | [`test_nested_transaction_rolls_back_to_a_savepoint`](../tests/end2end_sqlite3.rb#test_nested_transaction_rolls_back_to_a_savepoint), [`test_transaction_nonlocal_exit_semantics_follow_timeout_version`](../tests/end2end_sqlite3.rb#test_transaction_nonlocal_exit_semantics_follow_timeout_version) | [`test_nested_transactions_reject_isolation_and_mode`](../test/features_test.rb#test_nested_transactions_reject_isolation_and_mode) | -| Isolation applied; invalid isolation/mode rejected before begin; SQLite locks | [`test_transaction_isolation_level`](../tests/end2end_pg.rb#test_transaction_isolation_level) | [`test_transaction_isolation_level_is_applied`](../tests/end2end_mysql2.rb#test_transaction_isolation_level_is_applied) | [`test_transaction_mode`](../tests/end2end_sqlite3.rb#test_transaction_mode), [`test_immediate_and_exclusive_transactions_block_a_second_connection`](../tests/end2end_sqlite3.rb#test_immediate_and_exclusive_transactions_block_a_second_connection), [`test_unknown_transaction_mode_is_rejected_before_beginning`](../tests/end2end_sqlite3.rb#test_unknown_transaction_mode_is_rejected_before_beginning) | [`test_nested_transactions_reject_isolation_and_mode`](../test/features_test.rb#test_nested_transactions_reject_isolation_and_mode) | +| Isolation applied; invalid isolation/mode rejected with `ArgumentError` before begin; SQLite locks | [`test_transaction_isolation_level`](../tests/end2end_pg.rb#test_transaction_isolation_level) | [`test_transaction_isolation_level_is_applied`](../tests/end2end_mysql2.rb#test_transaction_isolation_level_is_applied) | [`test_transaction_mode`](../tests/end2end_sqlite3.rb#test_transaction_mode), [`test_immediate_and_exclusive_transactions_block_a_second_connection`](../tests/end2end_sqlite3.rb#test_immediate_and_exclusive_transactions_block_a_second_connection), [`test_unknown_transaction_mode_is_rejected_before_beginning`](../tests/end2end_sqlite3.rb#test_unknown_transaction_mode_is_rejected_before_beginning) | [`test_nested_transactions_reject_isolation_and_mode`](../test/features_test.rb#test_nested_transactions_reject_isolation_and_mode) | | Logger debug format, transaction logs, secret-free parameters | [`test_logger_receives_query_names_and_durations`](../tests/end2end_pg.rb#test_logger_receives_query_names_and_durations) | [`test_logger_receives_query_names_and_durations`](../tests/end2end_mysql2.rb#test_logger_receives_query_names_and_durations) | [`test_logger_receives_query_names_and_durations`](../tests/end2end_sqlite3.rb#test_logger_receives_query_names_and_durations) | [`test_logger_support`](../test/features_test.rb#test_logger_support) | | Fiber scheduler concurrency / MySQL no prepared-statement leaks | [`test_queries_run_concurrently_under_the_fiber_scheduler`](../tests/end2end_pg.rb#test_queries_run_concurrently_under_the_fiber_scheduler) | [`test_generated_queries_overlap_under_sync_including_transactions`](../tests/end2end_mysql2.rb#test_generated_queries_overlap_under_sync_including_transactions), [`test_generated_queries_do_not_create_server_side_prepared_statements`](../tests/end2end_mysql2.rb#test_generated_queries_do_not_create_server_side_prepared_statements) | [`test_fibers_sharing_a_database_take_turns`](../tests/end2end_sqlite3.rb#test_fibers_sharing_a_database_take_turns) | | | Duplicate result columns, unsupported options, invalid `:copyfrom` clauses and generation errors | E2E cannot reach generation-time rejection | E2E cannot reach generation-time rejection | E2E cannot reach generation-time rejection | [`test_duplicate_result_columns_are_rejected`](../test/features_test.rb#test_duplicate_result_columns_are_rejected), [`test_copyfrom_keeps_or_rejects_conflict_clauses`](../test/features_test.rb#test_copyfrom_keeps_or_rejects_conflict_clauses), [`test_unknown_options_are_rejected_with_a_hint`](../test/options_test.rb#test_unknown_options_are_rejected_with_a_hint), [`test_query_names_cannot_replace_generated_methods`](../test/features_test.rb#test_query_names_cannot_replace_generated_methods) | @@ -43,6 +43,7 @@ CI runs `test/coverage_map_test.rb`, which checks every linked test method exist | Interrupts at `BEGIN` / `COMMIT` (`Thread#raise`, raised by the statement) roll back and leave no open transaction | [`test_interrupts_around_begin_and_commit_never_leave_the_connection_in_a_transaction`](../tests/end2end_pg.rb#test_interrupts_around_begin_and_commit_never_leave_the_connection_in_a_transaction) | [`test_interrupts_around_begin_and_commit_never_leave_the_connection_in_a_transaction`](../tests/end2end_mysql2.rb#test_interrupts_around_begin_and_commit_never_leave_the_connection_in_a_transaction) | [`test_interrupts_around_begin_and_commit_never_leave_the_database_in_a_transaction`](../tests/end2end_sqlite3.rb#test_interrupts_around_begin_and_commit_never_leave_the_database_in_a_transaction) | | | PostgreSQL statements dropped by `DEALLOCATE ALL` / `DISCARD ALL` are prepared again | [`test_statements_are_prepared_again_after_deallocate_all`](../tests/end2end_pg.rb#test_statements_are_prepared_again_after_deallocate_all) | not MySQL (no server-side prepared statements) | not SQLite | [`test_statements_are_named_after_the_module_and_query_class`](../test/features_test.rb#test_statements_are_named_after_the_module_and_query_class) | | `:execresult` with `RETURNING` runs the statement | covered by `:execresult` semantics | covered by `:execresult` semantics | [`test_execresult_with_returning_runs_the_statement`](../tests/end2end_sqlite3.rb#test_execresult_with_returning_runs_the_statement) | | +| `:copyfrom` keeps the case of quoted / mixed-case table and column names (MySQL `LOAD DATA`) | [`test_copyfrom_keeps_the_case_of_table_and_column_names`](../tests/end2end_pg.rb#test_copyfrom_keeps_the_case_of_table_and_column_names) | [`test_copyfrom_keeps_the_case_of_table_and_column_names`](../tests/end2end_mysql2.rb#test_copyfrom_keeps_the_case_of_table_and_column_names) | [`test_copyfrom_keeps_the_case_of_table_and_column_names`](../tests/end2end_sqlite3.rb#test_copyfrom_keeps_the_case_of_table_and_column_names) | | | `:copyfrom` takes any `Enumerable` (empty, lazy, `Enumerator`) | [`test_copyfrom_takes_any_enumerable`](../tests/end2end_pg.rb#test_copyfrom_takes_any_enumerable) | [`test_copyfrom_takes_any_enumerable`](../tests/end2end_mysql2.rb#test_copyfrom_takes_any_enumerable) | [`test_copyfrom_takes_any_enumerable`](../tests/end2end_sqlite3.rb#test_copyfrom_takes_any_enumerable) | | | Query names like `HasAuthors` / `IsActive` keep their name and stay RuboCop-clean | [`test_predicate_like_query_names_are_kept`](../tests/end2end_pg.rb#test_predicate_like_query_names_are_kept) | [`test_predicate_like_query_names_are_kept`](../tests/end2end_mysql2.rb#test_predicate_like_query_names_are_kept) | [`test_predicate_like_query_names_are_kept`](../tests/end2end_sqlite3.rb#test_predicate_like_query_names_are_kept) | [`test_predicate_like_query_names_are_rubocop_clean`](../test/features_test.rb#test_predicate_like_query_names_are_rubocop_clean) | | SQLite `JSON` / `UUID` columns are `String`s, also numeric-looking text | not PostgreSQL | not MySQL | [`test_json_and_uuid_columns_return_strings_that_look_numeric`](../tests/end2end_sqlite3.rb#test_json_and_uuid_columns_return_strings_that_look_numeric) | | diff --git a/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index ee8809b..f20c8d3 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -207,6 +207,19 @@ SELECT * FROM name_cases WHERE id = sqlc.arg(id); -- name: TruncateNameCases :exec TRUNCATE TABLE name_cases; +-- name: CopyMixedCaseCopies :copyfrom +-- MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name. +INSERT INTO `MixedCaseCopies` (`Id`, `SomeValue`) VALUES (?, ?); + +-- name: CopyMixedCaseCopiesUnquoted :copyfrom +INSERT INTO MixedCaseCopies (SomeValue, Id) VALUES (?, ?); + +-- name: ListMixedCaseCopies :many +SELECT * FROM `MixedCaseCopies` ORDER BY `Id`; + +-- name: TruncateMixedCaseCopies :exec +TRUNCATE TABLE `MixedCaseCopies`; + -- name: GetUnnamedColumns :one SELECT 1, 'two'; diff --git a/examples/authors/mysql/schema.sql b/examples/authors/mysql/schema.sql index 9d2fcc7..cd4cab2 100644 --- a/examples/authors/mysql/schema.sql +++ b/examples/authors/mysql/schema.sql @@ -100,6 +100,8 @@ CREATE TABLE mysql_binding_values ( json_unicode JSON ); +CREATE TABLE `MixedCaseCopies` (`Id` BIGINT NOT NULL PRIMARY KEY, `SomeValue` TEXT); + CREATE TABLE name_cases ( id BIGINT NOT NULL PRIMARY KEY, `class` TEXT NOT NULL, diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index e16e37f..dcd3e04 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -243,6 +243,15 @@ SELECT * FROM name_cases WHERE id = sqlc.arg(id); -- name: TruncateNameCases :exec TRUNCATE TABLE name_cases; +-- name: CopyMixedCaseCopies :copyfrom +INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES ($1, $2); + +-- name: ListMixedCaseCopies :many +SELECT * FROM "MixedCaseCopies" ORDER BY "Id"; + +-- name: TruncateMixedCaseCopies :exec +TRUNCATE TABLE "MixedCaseCopies"; + -- name: GetUnnamedColumns :one SELECT 1, 'two'; diff --git a/examples/authors/postgresql/schema.sql b/examples/authors/postgresql/schema.sql index 9108fde..2c0518d 100644 --- a/examples/authors/postgresql/schema.sql +++ b/examples/authors/postgresql/schema.sql @@ -137,6 +137,8 @@ CREATE TABLE batch_types ( stamps timestamptz[] ); +CREATE TABLE "MixedCaseCopies" ("Id" BIGINT NOT NULL PRIMARY KEY, "SomeValue" TEXT); + CREATE TABLE name_cases ( id BIGINT PRIMARY KEY, "class" TEXT NOT NULL, diff --git a/examples/authors/sqlite/query.sql b/examples/authors/sqlite/query.sql index 88dcd2d..0290589 100644 --- a/examples/authors/sqlite/query.sql +++ b/examples/authors/sqlite/query.sql @@ -200,6 +200,12 @@ SELECT * FROM name_cases WHERE id = sqlc.arg(id); -- name: TruncateNameCases :exec DELETE FROM name_cases; +-- name: CopyMixedCaseCopies :copyfrom +INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES (?, ?); + +-- name: ListMixedCaseCopies :many +SELECT * FROM "MixedCaseCopies" ORDER BY "Id"; + -- name: GetUnnamedColumns :one SELECT 1, 'two'; diff --git a/examples/authors/sqlite/schema.sql b/examples/authors/sqlite/schema.sql index f7d1432..14f329d 100644 --- a/examples/authors/sqlite/schema.sql +++ b/examples/authors/sqlite/schema.sql @@ -35,6 +35,8 @@ CREATE TABLE tags ( uses INTEGER NOT NULL DEFAULT 0 ); +CREATE TABLE "MixedCaseCopies" ("Id" INTEGER NOT NULL PRIMARY KEY, "SomeValue" TEXT); + CREATE TABLE name_cases ( id INTEGER PRIMARY KEY, "class" TEXT NOT NULL, diff --git a/examples/mysql2/models.rb b/examples/mysql2/models.rb index a15f694..f4fd019 100644 --- a/examples/mysql2/models.rb +++ b/examples/mysql2/models.rb @@ -117,6 +117,9 @@ class MysqlBindingValu < ::Data.define( end # rubocop:enable Naming/VariableNumber + class Mixedcasecopy < ::Data.define(:id, :somevalue) + end + class NameCase < ::Data.define( :id, :class_, diff --git a/examples/mysql2/models.rbs b/examples/mysql2/models.rbs index b79b204..7743bff 100644 --- a/examples/mysql2/models.rbs +++ b/examples/mysql2/models.rbs @@ -193,6 +193,12 @@ module Mysql2Codegen ) -> void end + class Mixedcasecopy + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/examples/mysql2/query_sql.rb b/examples/mysql2/query_sql.rb index 3b1a875..35574f6 100644 --- a/examples/mysql2/query_sql.rb +++ b/examples/mysql2/query_sql.rb @@ -452,6 +452,23 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :somevalue) + end + + class CopyMixedCaseCopiesUnquotedArgs < ::Data.define(:somevalue, :id) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT id, somevalue FROM `MixedCaseCopies` ORDER BY `Id` + SQL + + class ListMixedCaseCopiesRow < ::Data.define(:id, :somevalue) + end + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE `MixedCaseCopies` + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -519,6 +536,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1245,6 +1264,49 @@ def truncate_name_cases end end + # MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name. + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + ::Tempfile.create(['copy_mixed_case_copies', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.id, row.somevalue])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`Id`, `SomeValue`)") if file.pos.positive? + end + nil + end + end + + def copy_mixed_case_copies_unquoted(rows) + with_client('copy_mixed_case_copies_unquoted') do |client| + ::Tempfile.create(['copy_mixed_case_copies_unquoted', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.somevalue, row.id])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`SomeValue`, `Id`)") if file.pos.positive? + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = client.query(bind_params(client, LIST_MIXED_CASE_COPIES_SQL, []), as: :array) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + client.query(bind_params(client, TRUNCATE_MIXED_CASE_COPIES_SQL, [])) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = client.query(bind_params(client, GET_UNNAMED_COLUMNS_SQL, []), as: :array) diff --git a/examples/mysql2/query_sql.rbs b/examples/mysql2/query_sql.rbs index 6d27522..d139112 100644 --- a/examples/mysql2/query_sql.rbs +++ b/examples/mysql2/query_sql.rbs @@ -496,6 +496,28 @@ module Mysql2Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + + class CopyMixedCaseCopiesUnquotedArgs + attr_reader somevalue: ::String? + attr_reader id: ::Integer + def initialize: ( ::String? somevalue, ::Integer id) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -676,6 +698,14 @@ module Mysql2Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def copy_mixed_case_copies_unquoted: (::Array[CopyMixedCaseCopiesUnquotedArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/examples/pg/models.rb b/examples/pg/models.rb index cec6c8f..abe9f35 100644 --- a/examples/pg/models.rb +++ b/examples/pg/models.rb @@ -129,6 +129,11 @@ class BatchType < ::Data.define( ) end + # rubocop:disable Naming/MethodName + class MixedCaseCopy < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/examples/pg/models.rbs b/examples/pg/models.rbs index b16eb80..c4bc31f 100644 --- a/examples/pg/models.rbs +++ b/examples/pg/models.rbs @@ -214,6 +214,12 @@ module PgCodegen ) -> void end + class MixedCaseCopy + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/examples/pg/query_sql.rb b/examples/pg/query_sql.rb index 9e3e6d7..5492406 100644 --- a/examples/pg/query_sql.rb +++ b/examples/pg/query_sql.rb @@ -539,6 +539,22 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT "Id", "SomeValue" FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE "MixedCaseCopies" + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -610,6 +626,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1459,6 +1477,34 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + encoder = ::PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY "MixedCaseCopies" ("Id", "SomeValue") FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.id, row.some_value]) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = run_prepared(client, 'PgCodegen::QuerySql#list_mixed_case_copies', LIST_MIXED_CASE_COPIES_SQL) + result.map do |row| + ListMixedCaseCopiesRow.new(row['Id'], row['SomeValue']) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + run_prepared(client, 'PgCodegen::QuerySql#truncate_mixed_case_copies', TRUNCATE_MIXED_CASE_COPIES_SQL) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = run_prepared(client, 'PgCodegen::QuerySql#get_unnamed_columns', GET_UNNAMED_COLUMNS_SQL).values diff --git a/examples/pg/query_sql.rbs b/examples/pg/query_sql.rbs index e05f7f6..353521a 100644 --- a/examples/pg/query_sql.rbs +++ b/examples/pg/query_sql.rbs @@ -568,6 +568,22 @@ module PgCodegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader some_value: ::String? + def initialize: ( ::Integer id, ::String? some_value) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -778,6 +794,12 @@ module PgCodegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/examples/sqlite3/models.rb b/examples/sqlite3/models.rb index 3446141..3bbc176 100644 --- a/examples/sqlite3/models.rb +++ b/examples/sqlite3/models.rb @@ -26,6 +26,11 @@ class Regression < ::Data.define(:id, :order, :price, :payload) class Tag < ::Data.define(:name, :uses) end + # rubocop:disable Naming/MethodName + class MixedCaseCopies < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/examples/sqlite3/models.rbs b/examples/sqlite3/models.rbs index 798625e..be0fe0b 100644 --- a/examples/sqlite3/models.rbs +++ b/examples/sqlite3/models.rbs @@ -48,6 +48,12 @@ module Sqlite3Codegen def initialize: ( ::String name, ::Integer uses) -> void end + class MixedCaseCopies + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index 00150ef..d5c6dcf 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -376,6 +376,18 @@ class GetHTTPServerRow < ::Data.define( DELETE FROM name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT Id, SomeValue FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -1250,6 +1262,32 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + atomically(client) do + rows.each_slice(499) do |batch| + sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + stmt = client.prepare(LIST_MIXED_CASE_COPIES_SQL) + begin + result = execute_statement(stmt) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + ensure + stmt.close + end + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| stmt = client.prepare(GET_UNNAMED_COLUMNS_SQL) diff --git a/examples/sqlite3/query_sql.rbs b/examples/sqlite3/query_sql.rbs index 3f602f1..7f5aa9b 100644 --- a/examples/sqlite3/query_sql.rbs +++ b/examples/sqlite3/query_sql.rbs @@ -375,6 +375,20 @@ module Sqlite3Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader some_value: ::String? + def initialize: ( ::Integer id, ::String? some_value) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -544,6 +558,10 @@ module Sqlite3Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/lib/sqlc_gen_ruby/code.rb b/lib/sqlc_gen_ruby/code.rb index ce9a3ee..670d175 100644 --- a/lib/sqlc_gen_ruby/code.rb +++ b/lib/sqlc_gen_ruby/code.rb @@ -170,7 +170,7 @@ def comment(text) # GetAuthor => GET_AUTHOR_SQL # Single-quoted Ruby String literal of any text (`it's` -> 'it\\'s'). - def string_literal(text) = "'#{text.gsub(/['\\\\]/) { |c| "\\#{c}" }}'" + def string_literal(text) = "'#{text.gsub(/['\\]/) { |c| "\\#{c}" }}'" def sql_constant(query_name) = "#{snake_case(query_name).upcase}_SQL" diff --git a/lib/sqlc_gen_ruby/drivers/base.rb b/lib/sqlc_gen_ruby/drivers/base.rb index 18b4d52..a356b4c 100644 --- a/lib/sqlc_gen_ruby/drivers/base.rb +++ b/lib/sqlc_gen_ruby/drivers/base.rb @@ -89,6 +89,12 @@ def prepares_batch_rows? = false # SQL identifier quoting for the statements the plugin writes itself (:copyfrom). def quote_identifier(name) = %("#{name.gsub('"', '""')}") + # Quoted table and column names of a :copyfrom statement (table: sqlc's Identifier of the INSERT target). + def copy_target(_sql, table, columns) + name = table.schema.to_s.empty? ? '' : "#{quote_identifier(table.schema)}." + [name + quote_identifier(table.name), columns.map { |c| quote_identifier(c) }] + end + # Ruby expression that turns a parameter value into what the driver can bind (e.g. bytea escaping). def encode_param(expr, _column) = expr diff --git a/lib/sqlc_gen_ruby/drivers/mysql2.rb b/lib/sqlc_gen_ruby/drivers/mysql2.rb index 648fe18..7e569f3 100644 --- a/lib/sqlc_gen_ruby/drivers/mysql2.rb +++ b/lib/sqlc_gen_ruby/drivers/mysql2.rb @@ -57,6 +57,25 @@ def row_data_type = :array def quote_identifier(name) = "`#{name.gsub('`', '``')}`" + IDENTIFIER = /`(?:[^`]|``)*`|[^\s`.,()]+/ + + # sqlc lowercases MySQL identifiers, but table names are case-sensitive on Linux: LOAD DATA names the table + # and columns as the INSERT writes them (`MixedCase`). + def copy_target(sql, table, columns) + target = sql.gsub(%r{--[^\n]*|/\*.*?\*/}m, ' ') + .match(/\bINTO\s+(#{IDENTIFIER}(?:\s*\.\s*#{IDENTIFIER})?)\s*\(([^)]*)\)/i) + return super unless target + + *schema, name = target[1].scan(IDENTIFIER).map { |i| unquote_identifier(i) } + return super unless name.casecmp?(table.name) + + written = target[2].scan(IDENTIFIER).to_h { |c| [unquote_identifier(c).downcase, unquote_identifier(c)] } + [[*schema, name].map { |n| quote_identifier(n) }.join('.'), + columns.map { |c| quote_identifier(written.fetch(c.downcase, c)) }] + end + + def unquote_identifier(identifier) = identifier.start_with?('`') ? identifier[1...-1].gsub('``', '`') : identifier + def exec_rows_lines(exec, _query) = [exec, 'client.affected_rows'] def exec_last_id_lines(exec, _query) = [exec, 'client.last_id'] @@ -141,6 +160,8 @@ def self.connect(mysql2_params, logger: nil, **pool_options) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/lib/sqlc_gen_ruby/drivers/pg.rb b/lib/sqlc_gen_ruby/drivers/pg.rb index 9f1877c..f9defe5 100644 --- a/lib/sqlc_gen_ruby/drivers/pg.rb +++ b/lib/sqlc_gen_ruby/drivers/pg.rb @@ -282,6 +282,8 @@ def self.connect(pg_params, logger: nil, **pool_options) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/lib/sqlc_gen_ruby/method_gen.rb b/lib/sqlc_gen_ruby/method_gen.rb index 0771eaf..e33bf34 100644 --- a/lib/sqlc_gen_ruby/method_gen.rb +++ b/lib/sqlc_gen_ruby/method_gen.rb @@ -163,12 +163,10 @@ def last_id_type(query) def copy_statements(query, params) raise ArgumentError, "#{query.name}: :copyfrom without a target table" if query.insert_into_table.nil? - table = query.insert_into_table - name = table.schema.to_s.empty? ? '' : "#{@driver.quote_identifier(table.schema)}." - name += @driver.quote_identifier(table.name) + table, columns = @driver.copy_target(query.text, query.insert_into_table, params.map { |p| p.column.name }) values = params.map { |p| @driver.encode_param("row.#{p.name}", p.column) } verb, tail = insert_clauses(query.text) - @driver.copy_from_lines('rows', name, params.map { |p| @driver.quote_identifier(p.column.name) }, values, + @driver.copy_from_lines('rows', table, columns, values, Code.snake_case(query.name), query_name: query.name, sources: params.map(&:column), verb: verb, tail: tail) end diff --git a/steep_expectations.yml b/steep_expectations.yml index 8c91942..b149cf1 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,90 +3,90 @@ diagnostics: - range: start: - line: 507 + line: 524 character: 12 end: - line: 507 + line: 524 character: 26 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 507 + line: 524 character: 51 end: - line: 507 + line: 524 character: 57 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 507 + line: 524 character: 70 end: - line: 507 + line: 524 character: 85 severity: WARNING message: Empty kwargs doesn't have type annotation code: Ruby::UnannotatedEmptyCollection - range: start: - line: 532 + line: 551 character: 54 end: - line: 580 + line: 599 character: 11 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 544 + line: 563 character: 62 end: - line: 546 + line: 565 character: 15 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 552 + line: 571 character: 21 end: - line: 552 + line: 571 character: 27 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 555 + line: 574 character: 37 end: - line: 555 + line: 574 character: 43 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 570 + line: 589 character: 27 end: - line: 570 + line: 589 character: 33 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' code: Ruby::UnknownConstant - range: start: - line: 1416 + line: 1478 character: 20 end: - line: 1416 + line: 1478 character: 38 severity: ERROR message: |- @@ -99,20 +99,20 @@ code: Ruby::ArgumentTypeMismatch - range: start: - line: 1417 + line: 1479 character: 78 end: - line: 1417 + line: 1479 character: 84 severity: ERROR message: Type `(::String | nil)` does not have method `match?` code: Ruby::NoMethod - range: start: - line: 1419 + line: 1481 character: 20 end: - line: 1419 + line: 1481 character: 39 severity: ERROR message: |- @@ -125,10 +125,10 @@ code: Ruby::ArgumentTypeMismatch - range: start: - line: 1423 + line: 1485 character: 20 end: - line: 1423 + line: 1485 character: 39 severity: ERROR message: |- @@ -141,10 +141,10 @@ code: Ruby::ArgumentTypeMismatch - range: start: - line: 1428 + line: 1490 character: 20 end: - line: 1428 + line: 1490 character: 39 severity: ERROR message: |- @@ -157,10 +157,10 @@ code: Ruby::ArgumentTypeMismatch - range: start: - line: 1437 + line: 1499 character: 20 end: - line: 1437 + line: 1499 character: 24 severity: ERROR message: |- @@ -173,10 +173,10 @@ code: Ruby::ArgumentTypeMismatch - range: start: - line: 1470 + line: 1532 character: 18 end: - line: 1470 + line: 1532 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `Mysql2`' @@ -247,410 +247,420 @@ diagnostics: - range: start: - line: 598 + line: 614 character: 12 end: - line: 598 + line: 614 character: 26 severity: WARNING message: 'Cannot find the declaration of constant: `ConnectionPool`' code: Ruby::UnknownConstant - range: start: - line: 598 + line: 614 character: 51 end: - line: 598 + line: 614 character: 53 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 598 + line: 614 character: 62 end: - line: 598 + line: 614 character: 73 severity: WARNING message: Empty kwargs doesn't have type annotation code: Ruby::UnannotatedEmptyCollection - range: start: - line: 614 + line: 632 character: 51 end: - line: 614 + line: 632 character: 53 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 627 + line: 645 character: 54 end: - line: 671 + line: 689 character: 11 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 635 + line: 653 character: 62 end: - line: 637 + line: 655 character: 15 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 641 + line: 659 character: 60 end: - line: 641 + line: 659 character: 62 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 643 + line: 661 character: 76 end: - line: 643 + line: 661 character: 78 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 644 + line: 662 character: 65 end: - line: 644 + line: 662 character: 67 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 651 + line: 669 character: 37 end: - line: 651 + line: 669 character: 39 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 653 + line: 671 character: 22 end: - line: 653 + line: 671 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 656 + line: 674 character: 55 end: - line: 656 + line: 674 character: 57 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 662 + line: 680 character: 64 end: - line: 662 + line: 680 character: 66 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 663 + line: 681 character: 42 end: - line: 663 + line: 681 character: 44 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 663 + line: 681 character: 95 end: - line: 663 + line: 681 character: 97 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 668 + line: 686 character: 22 end: - line: 668 + line: 686 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 872 + line: 890 + character: 20 + end: + line: 890 + character: 22 + severity: WARNING + message: 'Cannot find the declaration of constant: `PG`' + code: Ruby::UnknownConstant + - range: + start: + line: 1268 character: 20 end: - line: 872 + line: 1268 character: 22 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1250 + line: 1348 character: 20 end: - line: 1250 + line: 1348 character: 22 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1330 + line: 1482 character: 20 end: - line: 1330 + line: 1482 character: 22 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1571 + line: 1617 character: 32 end: - line: 1571 + line: 1617 character: 34 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1573 + line: 1619 character: 20 end: - line: 1573 + line: 1619 character: 22 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1582 + line: 1628 character: 60 end: - line: 1582 + line: 1628 character: 62 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1594 + line: 1640 character: 19 end: - line: 1594 + line: 1640 character: 21 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1597 + line: 1643 character: 18 end: - line: 1597 + line: 1643 character: 20 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1599 + line: 1645 character: 55 end: - line: 1599 + line: 1645 character: 57 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1606 + line: 1652 character: 18 end: - line: 1606 + line: 1652 character: 20 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1607 + line: 1653 character: 16 end: - line: 1607 + line: 1653 character: 18 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1608 + line: 1654 character: 68 end: - line: 1608 + line: 1654 character: 70 severity: WARNING message: Empty array doesn't have type annotation code: Ruby::UnannotatedEmptyCollection - range: start: - line: 1621 + line: 1667 character: 13 end: - line: 1621 + line: 1667 character: 15 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1623 + line: 1669 character: 50 end: - line: 1623 + line: 1669 character: 52 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1633 + line: 1679 character: 52 end: - line: 1639 + line: 1685 character: 9 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 1636 + line: 1682 character: 15 end: - line: 1636 + line: 1682 character: 17 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1638 + line: 1684 character: 52 end: - line: 1638 + line: 1684 character: 54 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1644 + line: 1690 character: 17 end: - line: 1644 + line: 1690 character: 19 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1651 + line: 1697 character: 53 end: - line: 1651 + line: 1697 character: 55 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1667 + line: 1713 character: 8 end: - line: 1667 + line: 1713 character: 10 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1667 + line: 1713 character: 52 end: - line: 1667 + line: 1713 character: 54 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1674 + line: 1720 character: 44 end: - line: 1674 + line: 1720 character: 46 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1675 + line: 1721 character: 27 end: - line: 1675 + line: 1721 character: 29 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1676 + line: 1722 character: 46 end: - line: 1676 + line: 1722 character: 48 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' code: Ruby::UnknownConstant - range: start: - line: 1677 + line: 1723 character: 27 end: - line: 1677 + line: 1723 character: 29 severity: WARNING message: 'Cannot find the declaration of constant: `PG`' @@ -659,30 +669,30 @@ diagnostics: - range: start: - line: 461 + line: 473 character: 56 end: - line: 489 + line: 501 character: 13 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 467 + line: 479 character: 64 end: - line: 469 + line: 481 character: 17 severity: WARNING message: The method cannot be called with a block code: Ruby::UnexpectedBlockGiven - range: start: - line: 1407 + line: 1445 character: 17 end: - line: 1407 + line: 1445 character: 24 severity: WARNING message: 'Cannot find the declaration of constant: `SQLite3`' diff --git a/test/fixtures/mysql2.bin b/test/fixtures/mysql2.bin index f52a0e6..3b4fdf6 100644 --- a/test/fixtures/mysql2.bin +++ b/test/fixtures/mysql2.bin @@ -1,7 +1,7 @@ -2mysql×(public"Ì(public€ +2mysqlÏ)public"Ä)public€ authors& id0ÿÿÿÿÿÿÿÿÿR authorsbbigint& name0ÿÿÿÿÿÿÿÿÿR authorsbtext# @@ -117,7 +117,10 @@ year_value0 time_value0 Rmysql_binding_valuesbtime9 - json_unicode0ÿÿÿÿÿÿÿÿÿRmysql_binding_valuesbjson¤ + json_unicode0ÿÿÿÿÿÿÿÿÿRmysql_binding_valuesbjsonv +mixedcasecopies. +id0ÿÿÿÿÿÿÿÿÿRmixedcasecopiesbbigint1 + somevalue0ÿÿÿÿÿÿÿÿÿRmixedcasecopiesbtext¤  name_cases) id0ÿÿÿÿÿÿÿÿÿR  @@ -648,7 +651,17 @@ name_casesbtextz initialize*3/ id0ÿÿÿÿÿÿÿÿÿ8R  name_casesbbigintzid: query.sql@ -TRUNCATE TABLE name_casesTruncateNameCases:exec: query.sqlb +TRUNCATE TABLE name_casesTruncateNameCases:exec: query.sqlü +?INSERT INTO `MixedCaseCopies` (`Id`, `SomeValue`) VALUES (?, ?)CopyMixedCaseCopies :copyfrom*>: +id0ÿÿÿÿÿÿÿÿÿRpublicmixedcasecopiesbbigintzid*HD + somevalue0ÿÿÿÿÿÿÿÿÿRpublicmixedcasecopiesbtextz somevalue2q MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name.: query.sqlBmixedcasecopies‹ +9INSERT INTO MixedCaseCopies (SomeValue, Id) VALUES (?, ?)CopyMixedCaseCopiesUnquoted :copyfrom*HD + somevalue0ÿÿÿÿÿÿÿÿÿRpublicmixedcasecopiesbtextz somevalue*>: +id0ÿÿÿÿÿÿÿÿÿRpublicmixedcasecopiesbbigintzid: query.sqlBmixedcasecopiesÔ +9SELECT id, somevalue FROM `MixedCaseCopies` ORDER BY `Id`ListMixedCaseCopies:many"2 +id0ÿÿÿÿÿÿÿÿÿRmixedcasecopiesbbigintzid"< + somevalue0ÿÿÿÿÿÿÿÿÿRmixedcasecopiesbtextz somevalue: query.sqlM + TRUNCATE TABLE `MixedCaseCopies`TruncateMixedCaseCopies:exec: query.sqlb SELECT 1, 'two'GetUnnamedColumns:one"0ÿÿÿÿÿÿÿÿÿbint"0ÿÿÿÿÿÿÿÿÿbtext: query.sqlQ SELECT 1 AS `it's`GetQuotedAlias:one" it's0ÿÿÿÿÿÿÿÿÿbint: query.sql¯ diff --git a/test/fixtures/pg.bin b/test/fixtures/pg.bin index 8ab1c49..0a6dee3 100644 --- a/test/fixtures/pg.bin +++ b/test/fixtures/pg.bin @@ -1,7 +1,7 @@  2 -postgresql†4public"�0publicÍ +postgresql‰5public" 1publicÍ authors) id0ÿÿÿÿÿÿÿÿÿR authorsb  bigserial& name0ÿÿÿÿÿÿÿÿÿR authorsbtextH @@ -126,7 +126,11 @@ pg_catalogbool+ pg_catalogint4ˆ? amounts 0ÿÿÿÿÿÿÿÿÿR  batch_typesb pg_catalognumericˆ6 -stamps 0ÿÿÿÿÿÿÿÿÿR  batch_typesb  timestamptzˆ» +stamps 0ÿÿÿÿÿÿÿÿÿR  batch_typesb  timestamptzˆ€ +MixedCaseCopies8 +Id0ÿÿÿÿÿÿÿÿÿRMixedCaseCopiesb +pg_catalogint81 + SomeValue0ÿÿÿÿÿÿÿÿÿRMixedCaseCopiesbtext»  name_cases3 id0ÿÿÿÿÿÿÿÿÿR  @@ -712,7 +716,15 @@ name_casesbtextz initialize*<8 id0ÿÿÿÿÿÿÿÿÿ8R  name_casesbpg_catalog.int8zid: query.sql@ -TRUNCATE TABLE name_casesTruncateNameCases:exec: query.sqlb +TRUNCATE TABLE name_casesTruncateNameCases:exec: query.sql” +AINSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES ($1, $2)CopyMixedCaseCopies :copyfrom*GC +Id0ÿÿÿÿÿÿÿÿÿRpublicMixedCaseCopiesbpg_catalog.int8zId*HD + SomeValue0ÿÿÿÿÿÿÿÿÿRpublicMixedCaseCopiesbtextz SomeValue: query.sqlBMixedCaseCopiesâ +=SELECT "Id", "SomeValue" FROM "MixedCaseCopies" ORDER BY "Id"ListMixedCaseCopies:many"< +Id0ÿÿÿÿÿÿÿÿÿRMixedCaseCopiesb +pg_catalogint8zId"< + SomeValue0ÿÿÿÿÿÿÿÿÿRMixedCaseCopiesbtextz SomeValue: query.sqlM + TRUNCATE TABLE "MixedCaseCopies"TruncateMixedCaseCopies:exec: query.sqlb SELECT 1, 'two'GetUnnamedColumns:one"0ÿÿÿÿÿÿÿÿÿbint"0ÿÿÿÿÿÿÿÿÿbtext: query.sqlQ SELECT 1 AS "it's"GetQuotedAlias:one" it's0ÿÿÿÿÿÿÿÿÿbint: query.sqlX diff --git a/test/fixtures/sqlite3.bin b/test/fixtures/sqlite3.bin index 86a2b9b38c4a5eaf5d940b3bdab1ca52537fb46d..ba946a565d5b2672583ce31599161a2cbf7ac609 100644 GIT binary patch delta 657 zcmX>$iE+bpMoBJiE=D6Ew&KE^%#u{0yFx-Nxrv#1N>_w7N(!;m*K!F<2`c$!R-~pl zCl;qV=NDwA7ApxEaxrd5y zW0PVDag7L(+N{m8(Z0TiiOb&8FW5CGL;+};zXFDP6f~4PQa$ zD`?p3DA;QX2?L#8iRKR}POD^~Xj)Nzu9iIqqmVVGuYh5YTMrBmEP;UJxhi02XnAlj z3Ay0#B*+*Xra-+~g$Nxh&cf2vqDnnrlsJJxwI0pcN>Vo&xh#WSeO#S^0g|Gl0Qa?m zTadpmmf-OZa&Zk(aEep_1(T4lPiAomnwe6pR=J6Jl}e_VK_Hw&*z-zuI6RIuRbYB~ J^9AQJRsdKF=PLjJ delta 46 zcmdlmo$=TtMoBJiE=D6Ew&KE^%#u{0dxAnNxrv#1N>>FpN(!-TUd6J}ezTif7ApWl C+YVX) diff --git a/test/golden/mysql2/default/models.rb b/test/golden/mysql2/default/models.rb index a15f694..f4fd019 100644 --- a/test/golden/mysql2/default/models.rb +++ b/test/golden/mysql2/default/models.rb @@ -117,6 +117,9 @@ class MysqlBindingValu < ::Data.define( end # rubocop:enable Naming/VariableNumber + class Mixedcasecopy < ::Data.define(:id, :somevalue) + end + class NameCase < ::Data.define( :id, :class_, diff --git a/test/golden/mysql2/default/models.rbs b/test/golden/mysql2/default/models.rbs index b79b204..7743bff 100644 --- a/test/golden/mysql2/default/models.rbs +++ b/test/golden/mysql2/default/models.rbs @@ -193,6 +193,12 @@ module Mysql2Codegen ) -> void end + class Mixedcasecopy + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/test/golden/mysql2/default/my_queries_sql.rb b/test/golden/mysql2/default/my_queries_sql.rb index b2cae4a..d655481 100644 --- a/test/golden/mysql2/default/my_queries_sql.rb +++ b/test/golden/mysql2/default/my_queries_sql.rb @@ -36,6 +36,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/default/other_sql.rb b/test/golden/mysql2/default/other_sql.rb index 98ff09a..e362120 100644 --- a/test/golden/mysql2/default/other_sql.rb +++ b/test/golden/mysql2/default/other_sql.rb @@ -49,6 +49,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/default/query_sql.rb b/test/golden/mysql2/default/query_sql.rb index 3e810b6..1f5636f 100644 --- a/test/golden/mysql2/default/query_sql.rb +++ b/test/golden/mysql2/default/query_sql.rb @@ -452,6 +452,23 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :somevalue) + end + + class CopyMixedCaseCopiesUnquotedArgs < ::Data.define(:somevalue, :id) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT id, somevalue FROM `MixedCaseCopies` ORDER BY `Id` + SQL + + class ListMixedCaseCopiesRow < ::Data.define(:id, :somevalue) + end + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE `MixedCaseCopies` + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -549,6 +566,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1275,6 +1294,49 @@ def truncate_name_cases end end + # MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name. + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + ::Tempfile.create(['copy_mixed_case_copies', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.id, row.somevalue])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`Id`, `SomeValue`)") if file.pos.positive? + end + nil + end + end + + def copy_mixed_case_copies_unquoted(rows) + with_client('copy_mixed_case_copies_unquoted') do |client| + ::Tempfile.create(['copy_mixed_case_copies_unquoted', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.somevalue, row.id])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`SomeValue`, `Id`)") if file.pos.positive? + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = client.query(bind_params(client, LIST_MIXED_CASE_COPIES_SQL, []), as: :array) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + client.query(bind_params(client, TRUNCATE_MIXED_CASE_COPIES_SQL, [])) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = client.query(bind_params(client, GET_UNNAMED_COLUMNS_SQL, []), as: :array) diff --git a/test/golden/mysql2/default/query_sql.rbs b/test/golden/mysql2/default/query_sql.rbs index bc5da8b..9916a66 100644 --- a/test/golden/mysql2/default/query_sql.rbs +++ b/test/golden/mysql2/default/query_sql.rbs @@ -496,6 +496,28 @@ module Mysql2Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + + class CopyMixedCaseCopiesUnquotedArgs + attr_reader somevalue: ::String? + attr_reader id: ::Integer + def initialize: ( ::String? somevalue, ::Integer id) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader id: ::Integer + attr_reader somevalue: ::String? + def initialize: ( ::Integer id, ::String? somevalue) -> void + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -711,6 +733,14 @@ module Mysql2Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def copy_mixed_case_copies_unquoted: (::Array[CopyMixedCaseCopiesUnquotedArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/mysql2/ruby31_gemfile/models.rb b/test/golden/mysql2/ruby31_gemfile/models.rb index ceb7f1c..fb30529 100644 --- a/test/golden/mysql2/ruby31_gemfile/models.rb +++ b/test/golden/mysql2/ruby31_gemfile/models.rb @@ -126,6 +126,8 @@ module RegressionsStep ) # rubocop:enable Naming/VariableNumber + Mixedcasecopy = ::Struct.new(:id, :somevalue, keyword_init: true) + NameCase = ::Struct.new( :id, :class_, diff --git a/test/golden/mysql2/ruby31_gemfile/models.rbs b/test/golden/mysql2/ruby31_gemfile/models.rbs index e23802b..115031b 100644 --- a/test/golden/mysql2/ruby31_gemfile/models.rbs +++ b/test/golden/mysql2/ruby31_gemfile/models.rbs @@ -267,6 +267,13 @@ module Mysql2Codegen ) -> MysqlBindingValu end + class Mixedcasecopy < ::Struct[untyped] + attr_accessor id: ::Integer + attr_accessor somevalue: ::String? + def initialize: ( id: ::Integer, somevalue: ::String?) -> void + def self.new: ( id: ::Integer, somevalue: ::String?) -> Mixedcasecopy + end + class NameCase < ::Struct[untyped] attr_accessor id: ::Integer attr_accessor class_: ::String diff --git a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb index 2aea1a5..30186c5 100644 --- a/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/my_queries_sql.rb @@ -36,6 +36,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/ruby31_gemfile/other_sql.rb b/test/golden/mysql2/ruby31_gemfile/other_sql.rb index 83c96e7..6af52eb 100644 --- a/test/golden/mysql2/ruby31_gemfile/other_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/other_sql.rb @@ -48,6 +48,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rb b/test/golden/mysql2/ruby31_gemfile/query_sql.rb index 9a445d7..b6e06b8 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rb +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rb @@ -449,6 +449,20 @@ module Mysql2Codegen TRUNCATE TABLE name_cases SQL + CopyMixedCaseCopiesArgs = ::Struct.new(:id, :somevalue, keyword_init: true) + + CopyMixedCaseCopiesUnquotedArgs = ::Struct.new(:somevalue, :id, keyword_init: true) + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT id, somevalue FROM `MixedCaseCopies` ORDER BY `Id` + SQL + + ListMixedCaseCopiesRow = ::Struct.new(:id, :somevalue, keyword_init: true) + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE `MixedCaseCopies` + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -545,6 +559,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1288,6 +1304,49 @@ def truncate_name_cases end end + # MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name. + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + ::Tempfile.create(['copy_mixed_case_copies', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.id, row.somevalue])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`Id`, `SomeValue`)") if file.pos.positive? + end + nil + end + end + + def copy_mixed_case_copies_unquoted(rows) + with_client('copy_mixed_case_copies_unquoted') do |client| + ::Tempfile.create(['copy_mixed_case_copies_unquoted', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.somevalue, row.id])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`SomeValue`, `Id`)") if file.pos.positive? + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = client.query(bind_params(client, LIST_MIXED_CASE_COPIES_SQL, []), as: :array) + result.map do |row| + ListMixedCaseCopiesRow.new(id: row[0], somevalue: row[1]) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + client.query(bind_params(client, TRUNCATE_MIXED_CASE_COPIES_SQL, [])) + nil + end + end + # rubocop:disable Naming/VariableNumber def get_unnamed_columns with_client('get_unnamed_columns') do |client| diff --git a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs index e7d0968..399400e 100644 --- a/test/golden/mysql2/ruby31_gemfile/query_sql.rbs +++ b/test/golden/mysql2/ruby31_gemfile/query_sql.rbs @@ -622,6 +622,31 @@ module Mysql2Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs < ::Struct[untyped] + attr_accessor id: ::Integer + attr_accessor somevalue: ::String? + def initialize: ( id: ::Integer, somevalue: ::String?) -> void + def self.new: ( id: ::Integer, somevalue: ::String?) -> CopyMixedCaseCopiesArgs + end + + class CopyMixedCaseCopiesUnquotedArgs < ::Struct[untyped] + attr_accessor somevalue: ::String? + attr_accessor id: ::Integer + def initialize: ( somevalue: ::String?, id: ::Integer) -> void + def self.new: ( somevalue: ::String?, id: ::Integer) -> CopyMixedCaseCopiesUnquotedArgs + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow < ::Struct[untyped] + attr_accessor id: ::Integer + attr_accessor somevalue: ::String? + def initialize: ( id: ::Integer, somevalue: ::String?) -> void + def self.new: ( id: ::Integer, somevalue: ::String?) -> ListMixedCaseCopiesRow + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow < ::Struct[untyped] @@ -852,6 +877,14 @@ module Mysql2Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def copy_mixed_case_copies_unquoted: (::Array[CopyMixedCaseCopiesUnquotedArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/mysql2/ruby32_notypes/models.rb b/test/golden/mysql2/ruby32_notypes/models.rb index a15f694..f4fd019 100644 --- a/test/golden/mysql2/ruby32_notypes/models.rb +++ b/test/golden/mysql2/ruby32_notypes/models.rb @@ -117,6 +117,9 @@ class MysqlBindingValu < ::Data.define( end # rubocop:enable Naming/VariableNumber + class Mixedcasecopy < ::Data.define(:id, :somevalue) + end + class NameCase < ::Data.define( :id, :class_, diff --git a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb index 2aea1a5..30186c5 100644 --- a/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/my_queries_sql.rb @@ -36,6 +36,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/ruby32_notypes/other_sql.rb b/test/golden/mysql2/ruby32_notypes/other_sql.rb index 588982a..0425a9b 100644 --- a/test/golden/mysql2/ruby32_notypes/other_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/other_sql.rb @@ -49,6 +49,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/mysql2/ruby32_notypes/query_sql.rb b/test/golden/mysql2/ruby32_notypes/query_sql.rb index 9d053b2..cb3c2e6 100644 --- a/test/golden/mysql2/ruby32_notypes/query_sql.rb +++ b/test/golden/mysql2/ruby32_notypes/query_sql.rb @@ -452,6 +452,23 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :somevalue) + end + + class CopyMixedCaseCopiesUnquotedArgs < ::Data.define(:somevalue, :id) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT id, somevalue FROM `MixedCaseCopies` ORDER BY `Id` + SQL + + class ListMixedCaseCopiesRow < ::Data.define(:id, :somevalue) + end + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE `MixedCaseCopies` + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -549,6 +566,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = !client.in_transaction raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1278,6 +1297,49 @@ def truncate_name_cases end end + # MySQL table names are case-sensitive on Linux: LOAD DATA must use `MixedCaseCopies`, not sqlc's lowercased name. + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + ::Tempfile.create(['copy_mixed_case_copies', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.id, row.somevalue])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`Id`, `SomeValue`)") if file.pos.positive? + end + nil + end + end + + def copy_mixed_case_copies_unquoted(rows) + with_client('copy_mixed_case_copies_unquoted') do |client| + ::Tempfile.create(['copy_mixed_case_copies_unquoted', '.csv']) do |file| + rows.each do |row| + file.write(csv_line([row.somevalue, row.id])) + end + file.flush + load_data(client, "LOAD DATA LOCAL INFILE '#{client.escape(file.path)}' INTO TABLE `MixedCaseCopies` CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' ESCAPED BY '\\\\' LINES TERMINATED BY '\\n' (`SomeValue`, `Id`)") if file.pos.positive? + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = client.query(bind_params(client, LIST_MIXED_CASE_COPIES_SQL, []), as: :array) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + client.query(bind_params(client, TRUNCATE_MIXED_CASE_COPIES_SQL, [])) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = client.query(bind_params(client, GET_UNNAMED_COLUMNS_SQL, []), as: :array) diff --git a/test/golden/pg/default/models.rb b/test/golden/pg/default/models.rb index cec6c8f..abe9f35 100644 --- a/test/golden/pg/default/models.rb +++ b/test/golden/pg/default/models.rb @@ -129,6 +129,11 @@ class BatchType < ::Data.define( ) end + # rubocop:disable Naming/MethodName + class MixedCaseCopy < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/test/golden/pg/default/models.rbs b/test/golden/pg/default/models.rbs index b16eb80..c4bc31f 100644 --- a/test/golden/pg/default/models.rbs +++ b/test/golden/pg/default/models.rbs @@ -214,6 +214,12 @@ module PgCodegen ) -> void end + class MixedCaseCopy + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/test/golden/pg/default/my_queries_sql.rb b/test/golden/pg/default/my_queries_sql.rb index 5cb5d4a..2eefac1 100644 --- a/test/golden/pg/default/my_queries_sql.rb +++ b/test/golden/pg/default/my_queries_sql.rb @@ -34,6 +34,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/default/other_sql.rb b/test/golden/pg/default/other_sql.rb index 83270ab..aa43191 100644 --- a/test/golden/pg/default/other_sql.rb +++ b/test/golden/pg/default/other_sql.rb @@ -47,6 +47,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/default/query_sql.rb b/test/golden/pg/default/query_sql.rb index 66d07bf..06c6681 100644 --- a/test/golden/pg/default/query_sql.rb +++ b/test/golden/pg/default/query_sql.rb @@ -539,6 +539,22 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT "Id", "SomeValue" FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE "MixedCaseCopies" + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -640,6 +656,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1489,6 +1507,34 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + encoder = ::PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY "MixedCaseCopies" ("Id", "SomeValue") FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.id, row.some_value]) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = run_prepared(client, 'PgCodegen::QuerySql#list_mixed_case_copies', LIST_MIXED_CASE_COPIES_SQL) + result.map do |row| + ListMixedCaseCopiesRow.new(row['Id'], row['SomeValue']) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + run_prepared(client, 'PgCodegen::QuerySql#truncate_mixed_case_copies', TRUNCATE_MIXED_CASE_COPIES_SQL) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = run_prepared(client, 'PgCodegen::QuerySql#get_unnamed_columns', GET_UNNAMED_COLUMNS_SQL).values diff --git a/test/golden/pg/default/query_sql.rbs b/test/golden/pg/default/query_sql.rbs index 9e8bea0..22c7f4c 100644 --- a/test/golden/pg/default/query_sql.rbs +++ b/test/golden/pg/default/query_sql.rbs @@ -568,6 +568,22 @@ module PgCodegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader some_value: ::String? + def initialize: ( ::Integer id, ::String? some_value) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -813,6 +829,12 @@ module PgCodegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/pg/ruby31_gemfile/models.rb b/test/golden/pg/ruby31_gemfile/models.rb index 93ff880..adff79d 100644 --- a/test/golden/pg/ruby31_gemfile/models.rb +++ b/test/golden/pg/ruby31_gemfile/models.rb @@ -130,6 +130,10 @@ module LateMood keyword_init: true ) + # rubocop:disable Naming/MethodName + MixedCaseCopy = ::Struct.new(:Id, :SomeValue, keyword_init: true) + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName NameCase = ::Struct.new( :id, diff --git a/test/golden/pg/ruby31_gemfile/models.rbs b/test/golden/pg/ruby31_gemfile/models.rbs index fb8d3c5..f2650ef 100644 --- a/test/golden/pg/ruby31_gemfile/models.rbs +++ b/test/golden/pg/ruby31_gemfile/models.rbs @@ -292,6 +292,13 @@ module PgCodegen ) -> BatchType end + class MixedCaseCopy < ::Struct[untyped] + attr_accessor Id: ::Integer + attr_accessor SomeValue: ::String? + def initialize: ( Id: ::Integer, SomeValue: ::String?) -> void + def self.new: ( Id: ::Integer, SomeValue: ::String?) -> MixedCaseCopy + end + class NameCase < ::Struct[untyped] attr_accessor id: ::Integer attr_accessor class_: ::String diff --git a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb index c842801..423e662 100644 --- a/test/golden/pg/ruby31_gemfile/my_queries_sql.rb +++ b/test/golden/pg/ruby31_gemfile/my_queries_sql.rb @@ -35,6 +35,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/ruby31_gemfile/other_sql.rb b/test/golden/pg/ruby31_gemfile/other_sql.rb index fd958ca..b091a5b 100644 --- a/test/golden/pg/ruby31_gemfile/other_sql.rb +++ b/test/golden/pg/ruby31_gemfile/other_sql.rb @@ -47,6 +47,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rb b/test/golden/pg/ruby31_gemfile/query_sql.rb index 454dfcc..887f150 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rb +++ b/test/golden/pg/ruby31_gemfile/query_sql.rb @@ -528,6 +528,20 @@ module PgCodegen TRUNCATE TABLE name_cases SQL + CopyMixedCaseCopiesArgs = ::Struct.new(:id, :some_value, keyword_init: true) + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT "Id", "SomeValue" FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + ListMixedCaseCopiesRow = ::Struct.new(:Id, :SomeValue, keyword_init: true) + # rubocop:enable Naming/MethodName + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE "MixedCaseCopies" + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -628,6 +642,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1495,6 +1511,34 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + encoder = ::PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY "MixedCaseCopies" ("Id", "SomeValue") FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.id, row.some_value]) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = run_prepared(client, 'PgCodegen::QuerySql#list_mixed_case_copies', LIST_MIXED_CASE_COPIES_SQL) + result.map do |row| + ListMixedCaseCopiesRow.new(Id: row['Id'], SomeValue: row['SomeValue']) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + run_prepared(client, 'PgCodegen::QuerySql#truncate_mixed_case_copies', TRUNCATE_MIXED_CASE_COPIES_SQL) + nil + end + end + # rubocop:disable Naming/VariableNumber def get_unnamed_columns with_client('get_unnamed_columns') do |client| diff --git a/test/golden/pg/ruby31_gemfile/query_sql.rbs b/test/golden/pg/ruby31_gemfile/query_sql.rbs index 246b77a..f95235f 100644 --- a/test/golden/pg/ruby31_gemfile/query_sql.rbs +++ b/test/golden/pg/ruby31_gemfile/query_sql.rbs @@ -722,6 +722,24 @@ module PgCodegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs < ::Struct[untyped] + attr_accessor id: ::Integer + attr_accessor some_value: ::String? + def initialize: ( id: ::Integer, some_value: ::String?) -> void + def self.new: ( id: ::Integer, some_value: ::String?) -> CopyMixedCaseCopiesArgs + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow < ::Struct[untyped] + attr_accessor Id: ::Integer + attr_accessor SomeValue: ::String? + def initialize: ( Id: ::Integer, SomeValue: ::String?) -> void + def self.new: ( Id: ::Integer, SomeValue: ::String?) -> ListMixedCaseCopiesRow + end + + TRUNCATE_MIXED_CASE_COPIES_SQL: ::String + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow < ::Struct[untyped] @@ -982,6 +1000,12 @@ module PgCodegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + + def truncate_mixed_case_copies: -> void + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/pg/ruby32_notypes/models.rb b/test/golden/pg/ruby32_notypes/models.rb index cec6c8f..abe9f35 100644 --- a/test/golden/pg/ruby32_notypes/models.rb +++ b/test/golden/pg/ruby32_notypes/models.rb @@ -129,6 +129,11 @@ class BatchType < ::Data.define( ) end + # rubocop:disable Naming/MethodName + class MixedCaseCopy < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/test/golden/pg/ruby32_notypes/my_queries_sql.rb b/test/golden/pg/ruby32_notypes/my_queries_sql.rb index a4078f5..322dc60 100644 --- a/test/golden/pg/ruby32_notypes/my_queries_sql.rb +++ b/test/golden/pg/ruby32_notypes/my_queries_sql.rb @@ -34,6 +34,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/ruby32_notypes/other_sql.rb b/test/golden/pg/ruby32_notypes/other_sql.rb index cc9c27c..4139084 100644 --- a/test/golden/pg/ruby32_notypes/other_sql.rb +++ b/test/golden/pg/ruby32_notypes/other_sql.rb @@ -47,6 +47,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost diff --git a/test/golden/pg/ruby32_notypes/query_sql.rb b/test/golden/pg/ruby32_notypes/query_sql.rb index ea9007c..344d94e 100644 --- a/test/golden/pg/ruby32_notypes/query_sql.rb +++ b/test/golden/pg/ruby32_notypes/query_sql.rb @@ -539,6 +539,22 @@ class GetHTTPServerRow < ::Data.define( TRUNCATE TABLE name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT "Id", "SomeValue" FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + + TRUNCATE_MIXED_CASE_COPIES_SQL = <<~'SQL' + TRUNCATE TABLE "MixedCaseCopies" + SQL + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -640,6 +656,8 @@ def initialize(db, logger: nil) # becomes a SAVEPOINT. Yields a copy of self whose queries run in the transaction, also from other fibers and # threads (Enumerator, Async tasks); it raises TransactionEnded when used after the transaction. def transaction(isolation: nil) + raise ::ArgumentError, "unknown isolation level: #{isolation.inspect}" unless isolation.nil? || ISOLATION_LEVELS.key?(isolation) + with_client('transaction') do |client| outermost = client.transaction_status == ::PG::PQTRANS_IDLE raise ::ArgumentError, 'isolation: cannot be set on a nested transaction' if isolation && !outermost @@ -1492,6 +1510,34 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + encoder = ::PG::TextEncoder::CopyRow.new(type_map: client.type_map_for_queries) + client.copy_data('COPY "MixedCaseCopies" ("Id", "SomeValue") FROM STDIN', encoder) do + rows.each do |row| + client.put_copy_data([row.id, row.some_value]) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + result = run_prepared(client, 'PgCodegen::QuerySql#list_mixed_case_copies', LIST_MIXED_CASE_COPIES_SQL) + result.map do |row| + ListMixedCaseCopiesRow.new(row['Id'], row['SomeValue']) + end + end + end + + def truncate_mixed_case_copies + with_client('truncate_mixed_case_copies') do |client| + run_prepared(client, 'PgCodegen::QuerySql#truncate_mixed_case_copies', TRUNCATE_MIXED_CASE_COPIES_SQL) + nil + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| result = run_prepared(client, 'PgCodegen::QuerySql#get_unnamed_columns', GET_UNNAMED_COLUMNS_SQL).values diff --git a/test/golden/sqlite3/default/models.rb b/test/golden/sqlite3/default/models.rb index 3446141..3bbc176 100644 --- a/test/golden/sqlite3/default/models.rb +++ b/test/golden/sqlite3/default/models.rb @@ -26,6 +26,11 @@ class Regression < ::Data.define(:id, :order, :price, :payload) class Tag < ::Data.define(:name, :uses) end + # rubocop:disable Naming/MethodName + class MixedCaseCopies < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/test/golden/sqlite3/default/models.rbs b/test/golden/sqlite3/default/models.rbs index 798625e..be0fe0b 100644 --- a/test/golden/sqlite3/default/models.rbs +++ b/test/golden/sqlite3/default/models.rbs @@ -48,6 +48,12 @@ module Sqlite3Codegen def initialize: ( ::String name, ::Integer uses) -> void end + class MixedCaseCopies + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + class NameCase attr_reader id: ::Integer attr_reader class_: ::String diff --git a/test/golden/sqlite3/default/query_sql.rb b/test/golden/sqlite3/default/query_sql.rb index 6e3e811..15bea99 100644 --- a/test/golden/sqlite3/default/query_sql.rb +++ b/test/golden/sqlite3/default/query_sql.rb @@ -376,6 +376,18 @@ class GetHTTPServerRow < ::Data.define( DELETE FROM name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT Id, SomeValue FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -1280,6 +1292,32 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + atomically(client) do + rows.each_slice(499) do |batch| + sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + stmt = client.prepare(LIST_MIXED_CASE_COPIES_SQL) + begin + result = execute_statement(stmt) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + ensure + stmt.close + end + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| stmt = client.prepare(GET_UNNAMED_COLUMNS_SQL) diff --git a/test/golden/sqlite3/default/query_sql.rbs b/test/golden/sqlite3/default/query_sql.rbs index ee394fe..641bdae 100644 --- a/test/golden/sqlite3/default/query_sql.rbs +++ b/test/golden/sqlite3/default/query_sql.rbs @@ -375,6 +375,20 @@ module Sqlite3Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs + attr_reader id: ::Integer + attr_reader some_value: ::String? + def initialize: ( ::Integer id, ::String? some_value) -> void + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow + attr_reader Id: ::Integer + attr_reader SomeValue: ::String? + def initialize: ( ::Integer Id, ::String? SomeValue) -> void + end + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow @@ -579,6 +593,10 @@ module Sqlite3Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rb b/test/golden/sqlite3/ruby31_gemfile/models.rb index d6674f8..5ae0a60 100644 --- a/test/golden/sqlite3/ruby31_gemfile/models.rb +++ b/test/golden/sqlite3/ruby31_gemfile/models.rb @@ -34,6 +34,10 @@ module Sqlite3Codegen Tag = ::Struct.new(:name, :uses, keyword_init: true) + # rubocop:disable Naming/MethodName + MixedCaseCopies = ::Struct.new(:Id, :SomeValue, keyword_init: true) + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName NameCase = ::Struct.new( :id, diff --git a/test/golden/sqlite3/ruby31_gemfile/models.rbs b/test/golden/sqlite3/ruby31_gemfile/models.rbs index 3c6516f..4ab8d3f 100644 --- a/test/golden/sqlite3/ruby31_gemfile/models.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/models.rbs @@ -61,6 +61,13 @@ module Sqlite3Codegen def self.new: ( name: ::String, uses: ::Integer) -> Tag end + class MixedCaseCopies < ::Struct[untyped] + attr_accessor Id: ::Integer + attr_accessor SomeValue: ::String? + def initialize: ( Id: ::Integer, SomeValue: ::String?) -> void + def self.new: ( Id: ::Integer, SomeValue: ::String?) -> MixedCaseCopies + end + class NameCase < ::Struct[untyped] attr_accessor id: ::Integer attr_accessor class_: ::String diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb index 5b6fb29..850fcca 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -358,6 +358,16 @@ module Sqlite3Codegen DELETE FROM name_cases SQL + CopyMixedCaseCopiesArgs = ::Struct.new(:id, :some_value, keyword_init: true) + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT Id, SomeValue FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + ListMixedCaseCopiesRow = ::Struct.new(:Id, :SomeValue, keyword_init: true) + # rubocop:enable Naming/MethodName + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -1270,6 +1280,32 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + atomically(client) do + rows.each_slice(499) do |batch| + sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + stmt = client.prepare(LIST_MIXED_CASE_COPIES_SQL) + begin + result = execute_statement(stmt) + result.map do |row| + ListMixedCaseCopiesRow.new(Id: row[0], SomeValue: row[1]) + end + ensure + stmt.close + end + end + end + # rubocop:disable Naming/VariableNumber def get_unnamed_columns with_client('get_unnamed_columns') do |client| diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs index 0ccc023..a96ef07 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rbs @@ -442,6 +442,22 @@ module Sqlite3Codegen TRUNCATE_NAME_CASES_SQL: ::String + class CopyMixedCaseCopiesArgs < ::Struct[untyped] + attr_accessor id: ::Integer + attr_accessor some_value: ::String? + def initialize: ( id: ::Integer, some_value: ::String?) -> void + def self.new: ( id: ::Integer, some_value: ::String?) -> CopyMixedCaseCopiesArgs + end + + LIST_MIXED_CASE_COPIES_SQL: ::String + + class ListMixedCaseCopiesRow < ::Struct[untyped] + attr_accessor Id: ::Integer + attr_accessor SomeValue: ::String? + def initialize: ( Id: ::Integer, SomeValue: ::String?) -> void + def self.new: ( Id: ::Integer, SomeValue: ::String?) -> ListMixedCaseCopiesRow + end + GET_UNNAMED_COLUMNS_SQL: ::String class GetUnnamedColumnsRow < ::Struct[untyped] @@ -662,6 +678,10 @@ module Sqlite3Codegen def truncate_name_cases: -> void + def copy_mixed_case_copies: (::Array[CopyMixedCaseCopiesArgs] rows) -> void + + def list_mixed_case_copies: -> ::Array[ListMixedCaseCopiesRow] + def get_unnamed_columns: -> GetUnnamedColumnsRow? def get_unnamed_columns!: -> GetUnnamedColumnsRow diff --git a/test/golden/sqlite3/ruby32_notypes/models.rb b/test/golden/sqlite3/ruby32_notypes/models.rb index 3446141..3bbc176 100644 --- a/test/golden/sqlite3/ruby32_notypes/models.rb +++ b/test/golden/sqlite3/ruby32_notypes/models.rb @@ -26,6 +26,11 @@ class Regression < ::Data.define(:id, :order, :price, :payload) class Tag < ::Data.define(:name, :uses) end + # rubocop:disable Naming/MethodName + class MixedCaseCopies < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + # rubocop:disable Naming/MethodName class NameCase < ::Data.define( :id, diff --git a/test/golden/sqlite3/ruby32_notypes/query_sql.rb b/test/golden/sqlite3/ruby32_notypes/query_sql.rb index fe0dc10..0264bf0 100644 --- a/test/golden/sqlite3/ruby32_notypes/query_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -376,6 +376,18 @@ class GetHTTPServerRow < ::Data.define( DELETE FROM name_cases SQL + class CopyMixedCaseCopiesArgs < ::Data.define(:id, :some_value) + end + + LIST_MIXED_CASE_COPIES_SQL = <<~'SQL' + SELECT Id, SomeValue FROM "MixedCaseCopies" ORDER BY "Id" + SQL + + # rubocop:disable Naming/MethodName + class ListMixedCaseCopiesRow < ::Data.define(:Id, :SomeValue) + end + # rubocop:enable Naming/MethodName + GET_UNNAMED_COLUMNS_SQL = <<~'SQL' SELECT 1, 'two' SQL @@ -1283,6 +1295,32 @@ def truncate_name_cases end end + def copy_mixed_case_copies(rows) + with_client('copy_mixed_case_copies') do |client| + atomically(client) do + rows.each_slice(499) do |batch| + sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) + end + end + nil + end + end + + def list_mixed_case_copies + with_client('list_mixed_case_copies') do |client| + stmt = client.prepare(LIST_MIXED_CASE_COPIES_SQL) + begin + result = execute_statement(stmt) + result.map do |row| + ListMixedCaseCopiesRow.new(row[0], row[1]) + end + ensure + stmt.close + end + end + end + def get_unnamed_columns with_client('get_unnamed_columns') do |client| stmt = client.prepare(GET_UNNAMED_COLUMNS_SQL) diff --git a/tests/end2end_mysql2.rb b/tests/end2end_mysql2.rb index 6ce5f4f..9e2dc45 100755 --- a/tests/end2end_mysql2.rb +++ b/tests/end2end_mysql2.rb @@ -88,6 +88,7 @@ def setup @query_sql.truncate_copy_types @query_sql.truncate_mysql_binding_values @query_sql.truncate_name_cases + @query_sql.truncate_mixed_case_copies @query_sql.truncate_exceptions end @@ -263,6 +264,13 @@ def test_copyfrom_many_rows_and_none assert_equal 2500, @query_sql.list_authors.size end + # sqlc lowercases MySQL names, but table names are case-sensitive on Linux: LOAD DATA must name `MixedCaseCopies` + def test_copyfrom_keeps_the_case_of_table_and_column_names + @query_sql.copy_mixed_case_copies([CopyMixedCaseCopiesArgs.new(id: 1, somevalue: 'a'), CopyMixedCaseCopiesArgs.new(id: 2, somevalue: nil)]) + @query_sql.copy_mixed_case_copies_unquoted([CopyMixedCaseCopiesUnquotedArgs.new(somevalue: 'c', id: 3)]) + assert_equal [[1, 'a'], [2, nil], [3, 'c']], @query_sql.list_mixed_case_copies.map { |r| r.to_h.values } + end + def test_copyfrom_takes_any_enumerable @query_sql.copy_authors([].lazy) assert_empty @query_sql.list_authors @@ -449,7 +457,7 @@ def test_transaction_isolation_level_is_applied end assert_equal ['SERIALIZABLE', 'ACTIVE'], row.values end - assert_raises(KeyError) { @query_sql.transaction(isolation: :bogus) { nil } } + assert_raises(ArgumentError) { @query_sql.transaction(isolation: :bogus) { flunk } } @query_sql.instance_variable_get(:@db).with do |client| rows = client.query(<<~SQL) SELECT STATE diff --git a/tests/end2end_pg.rb b/tests/end2end_pg.rb index a5b89cc..4822245 100755 --- a/tests/end2end_pg.rb +++ b/tests/end2end_pg.rb @@ -91,6 +91,7 @@ def setup @query_sql.truncate_regressions @query_sql.truncate_batch_types @query_sql.truncate_name_cases + @query_sql.truncate_mixed_case_copies @query_sql.truncate_json_round_trips @query_sql.truncate_deferred_books @query_sql.truncate_exceptions @@ -233,6 +234,11 @@ def test_copyfrom_many_rows_and_none assert_equal 2500, @query_sql.list_authors.size end + def test_copyfrom_keeps_the_case_of_table_and_column_names + @query_sql.copy_mixed_case_copies([CopyMixedCaseCopiesArgs.new(id: 1, some_value: 'a'), CopyMixedCaseCopiesArgs.new(id: 2, some_value: nil)]) + assert_equal [[1, 'a'], [2, nil]], @query_sql.list_mixed_case_copies.map { |r| r.to_h.values } + end + def test_copyfrom_takes_any_enumerable @query_sql.copy_authors([].lazy) assert_empty @query_sql.list_authors @@ -589,7 +595,7 @@ def test_transaction_isolation_level level = @query_sql.instance_variable_get(:@db).with { |c| c.exec('SHOW transaction_isolation').getvalue(0, 0) } assert_equal 'serializable', level end - assert_raises(KeyError) { @query_sql.transaction(isolation: :bogus) { nil } } + assert_raises(ArgumentError) { @query_sql.transaction(isolation: :bogus) { flunk } } @query_sql.instance_variable_get(:@db).with { |c| assert_equal PG::PQTRANS_IDLE, c.transaction_status } end diff --git a/tests/end2end_sqlite3.rb b/tests/end2end_sqlite3.rb index 4f51682..d65e70d 100644 --- a/tests/end2end_sqlite3.rb +++ b/tests/end2end_sqlite3.rb @@ -309,6 +309,11 @@ def test_copyfrom_is_batched_below_the_bound_parameter_limit assert_equal 2500, @query_sql.list_authors.size end + def test_copyfrom_keeps_the_case_of_table_and_column_names + @query_sql.copy_mixed_case_copies([CopyMixedCaseCopiesArgs.new(id: 1, some_value: 'a'), CopyMixedCaseCopiesArgs.new(id: 2, some_value: nil)]) + assert_equal [[1, 'a'], [2, nil]], @query_sql.list_mixed_case_copies.map { |r| r.to_h.values } + end + def test_copyfrom_takes_any_enumerable @query_sql.copy_authors([].lazy) assert_empty @query_sql.list_authors From 7a2cb9fdf03db213e27dc256c80f64f671426662 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:36:41 +0000 Subject: [PATCH 2/2] SQLite :copyfrom: unquote the table name sqlc 1.27 reports with its quotes Co-Authored-By: Doron Eli Rachman --- examples/sqlite3/query_sql.rb | 2 +- lib/sqlc_gen_ruby/drivers/sqlite3.rb | 13 +++++++++++++ test/golden/sqlite3/default/query_sql.rb | 2 +- test/golden/sqlite3/ruby31_gemfile/query_sql.rb | 2 +- test/golden/sqlite3/ruby32_notypes/query_sql.rb | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/sqlite3/query_sql.rb b/examples/sqlite3/query_sql.rb index d5c6dcf..7e4bf9a 100644 --- a/examples/sqlite3/query_sql.rb +++ b/examples/sqlite3/query_sql.rb @@ -1266,7 +1266,7 @@ def copy_mixed_case_copies(rows) with_client('copy_mixed_case_copies') do |client| atomically(client) do rows.each_slice(499) do |batch| - sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + sql = %(INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) end end diff --git a/lib/sqlc_gen_ruby/drivers/sqlite3.rb b/lib/sqlc_gen_ruby/drivers/sqlite3.rb index 3852cf5..6249055 100644 --- a/lib/sqlc_gen_ruby/drivers/sqlite3.rb +++ b/lib/sqlc_gen_ruby/drivers/sqlite3.rb @@ -203,6 +203,19 @@ def exec_last_id_lines(exec, query) ["result = #{exec}", 'row = result.first', 'row && row[0]'] end + # sqlc (1.27 at least) keeps the quotes of a quoted SQLite name (`"MixedCase"`): quote the bare name once. + def copy_target(_sql, table, columns) + names = [table.schema, table.name].reject { |n| n.to_s.empty? }.map { |n| quote_identifier(unquote_identifier(n)) } + [names.join('.'), columns.map { |c| quote_identifier(unquote_identifier(c)) }] + end + + def unquote_identifier(name) + return name[1...-1] if name.start_with?('[') && name.end_with?(']') + return name unless name.length > 1 && %w[" `].include?(name[0]) && name.end_with?(name[0]) + + name[1...-1].gsub(name[0] * 2, name[0]) + end + # One multi-row `INSERT ... VALUES (?, ?), (?, ?), ...` per batch. # The conflict clause is kept: INSERT OR REPLACE / OR IGNORE, REPLACE, ON CONFLICT ... DO ... (without parameters). def copy_from_lines(rows_var, table, columns, values, _label, query_name:, sources:, verb:, tail:) diff --git a/test/golden/sqlite3/default/query_sql.rb b/test/golden/sqlite3/default/query_sql.rb index 15bea99..772de29 100644 --- a/test/golden/sqlite3/default/query_sql.rb +++ b/test/golden/sqlite3/default/query_sql.rb @@ -1296,7 +1296,7 @@ def copy_mixed_case_copies(rows) with_client('copy_mixed_case_copies') do |client| atomically(client) do rows.each_slice(499) do |batch| - sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + sql = %(INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) end end diff --git a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb index 850fcca..a167a28 100644 --- a/test/golden/sqlite3/ruby31_gemfile/query_sql.rb +++ b/test/golden/sqlite3/ruby31_gemfile/query_sql.rb @@ -1284,7 +1284,7 @@ def copy_mixed_case_copies(rows) with_client('copy_mixed_case_copies') do |client| atomically(client) do rows.each_slice(499) do |batch| - sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + sql = %(INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) end end diff --git a/test/golden/sqlite3/ruby32_notypes/query_sql.rb b/test/golden/sqlite3/ruby32_notypes/query_sql.rb index 0264bf0..3685cb0 100644 --- a/test/golden/sqlite3/ruby32_notypes/query_sql.rb +++ b/test/golden/sqlite3/ruby32_notypes/query_sql.rb @@ -1299,7 +1299,7 @@ def copy_mixed_case_copies(rows) with_client('copy_mixed_case_copies') do |client| atomically(client) do rows.each_slice(499) do |batch| - sql = %(INSERT INTO """MixedCaseCopies""" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) + sql = %(INSERT INTO "MixedCaseCopies" ("Id", "SomeValue") VALUES #{::Array.new(batch.size, '(?, ?)').join(', ')}) client.execute(sql, batch.flat_map { |row| [row.id, row.some_value] }) end end