Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ More: [the generated code for every driver](docs/Examples.md) (it is the plugin'
| `overrides` | none | Change the type of columns / parameters, see [below](#overrides). |
| `rename` | none | Rename tables (model class) and columns (attribute), e.g. `{authors: Writer, bio: biography}`; `schema.table` works too. |
| `exactTableNames` | `false` | Keep table names as model names (`authors` → `Authors`) instead of singularizing them (`Author`). |
| `preparedStatements` | `true` | `pg` only (an error with other drivers). `false` sends the SQL with every call instead of preparing it (needed behind pgbouncer in transaction mode). Statements dropped by `DEALLOCATE ALL` / `DISCARD ALL` are prepared again by their next call outside a transaction (inside one, that call raises and the transaction rolls back). Statements are named `Module::Class#method` (with a hash when longer than PostgreSQL's 63 bytes), so classes sharing a pool never run each other's SQL. |
| `preparedStatements` | `true` | `pg` only (an error with other drivers). `false` sends the SQL with every call instead of preparing it (needed behind pgbouncer in transaction mode). Statements dropped by `DEALLOCATE ALL` / `DISCARD ALL`, and statements PostgreSQL refuses after a migration changed the type of a column they return (`PG::FeatureNotSupported`: cached plan must not change result type), are prepared again by their next call outside a transaction (inside one, that call raises and the transaction rolls back; the next call prepares the statement again). Statements are named `Module::Class#method` (with a hash when longer than PostgreSQL's 63 bytes), so classes sharing a pool never run each other's SQL. |

Unknown or misspelled options are errors, with a hint for near misses (`generate_types` → `generateTypes`).
Options shared by several `sql:` blocks can go in the top-level `options:` of `sqlc.yaml` under the plugin's name
Expand Down Expand Up @@ -125,6 +125,8 @@ end
and rolls back when it raises or the thread is killed. An interrupt (`Timeout`, `Thread#raise`, `Thread#kill`) that
arrives while `BEGIN`, `COMMIT` or `ROLLBACK` runs waits for it to finish, and a `BEGIN` / `COMMIT` that raises
itself (a stopped `Async` task) is rolled back: the connection never goes back to the pool inside a transaction.
On PostgreSQL a query interrupted by any of them (inside or outside a transaction) is cancelled at once, so the
connection goes back to the pool idle and a killed transaction rolls back without waiting for its query.
On MySQL a `Thread#kill` during a query closes that client (mysql2 cannot send `ROLLBACK` while it waits for the
query's result), so the server rolls the transaction back and the pool replaces the client. On Ruby <3.3 with timeout <0.4, `break`, `return`, and
`throw` also roll back because Timeout cannot be told apart; add `gem 'timeout', '>= 0.4'` to the generated Gemfile
Expand Down Expand Up @@ -195,6 +197,10 @@ server; duplicate keys and invalid values raise `Mysql2::Error` instead of being
in the zone `mysql2` reads `DATETIME` / `TIMESTAMP` values back in, the process's local zone, or UTC for a client
created with `database_timezone: :utc` (`connect(params.merge(database_timezone: :utc))`). The server also converts
`TIMESTAMP` columns with the session `time_zone`, so keep that equal to the client's zone.
* PostgreSQL: a `Time` parameter of a `timestamp` (without time zone) column (also in `:copyfrom` and batches) keeps
its instant whatever its UTC offset: it is written in the process's local zone, the zone the pg gem reads
`timestamp` values back in (PostgreSQL itself ignores the offset of a `timestamp` value). Elements of `timestamp[]`
arrays are written as given. `timestamptz` keeps the instant anyway.
* A `String` passed to a PostgreSQL `json` / `jsonb` parameter is JSON text (`'{"a":1}'`): pass `'"hi"'` or
`'hi'.to_json` for a JSON string. Hashes, Arrays, numbers, `true` / `false` and `nil` are encoded for you.
* Enums are `String`s, with constants in `models.rb` (`ExtendedBioType::MEMOIR`, `ExtendedBioType::ALL`). Values that
Expand Down
6 changes: 3 additions & 3 deletions docs/Coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ only when the behavior is rejected or decided during generation and generated co
| `sqlc.embed`, outer-join nil, same table embedded twice | [`test_embed`](../tests/end2end_pg.rb#test_embed), [`test_embed_on_the_outer_side_of_a_join_is_nil_without_a_match`](../tests/end2end_pg.rb#test_embed_on_the_outer_side_of_a_join_is_nil_without_a_match), [`test_same_table_embedded_twice`](../tests/end2end_pg.rb#test_same_table_embedded_twice) | [`test_embed`](../tests/end2end_mysql2.rb#test_embed), [`test_outer_join_embed_is_nil_without_a_match`](../tests/end2end_mysql2.rb#test_outer_join_embed_is_nil_without_a_match), [`test_same_table_embedded_twice`](../tests/end2end_mysql2.rb#test_same_table_embedded_twice) | [`test_embed`](../tests/end2end_sqlite3.rb#test_embed), [`test_outer_join_embed_is_nil_without_a_match`](../tests/end2end_sqlite3.rb#test_outer_join_embed_is_nil_without_a_match), [`test_same_table_embedded_twice`](../tests/end2end_sqlite3.rb#test_same_table_embedded_twice) | [`test_embedded_tables_become_model_attributes`](../test/features_test.rb#test_embedded_tables_become_model_attributes) |
| Frozen Ruby 3.2+ `Data`, mutable older `Struct`, `with`, `to_h`, pattern matching | [`test_rows_and_embedded_models_have_supported_layout_and_pattern_matching`](../tests/end2end_pg.rb#test_rows_and_embedded_models_have_supported_layout_and_pattern_matching) | [`test_rows_and_embedded_models_have_supported_layout_and_pattern_matching`](../tests/end2end_mysql2.rb#test_rows_and_embedded_models_have_supported_layout_and_pattern_matching) | [`test_rows_and_embedded_models_have_supported_layout_and_pattern_matching`](../tests/end2end_sqlite3.rb#test_rows_and_embedded_models_have_supported_layout_and_pattern_matching) | [`test_struct_classes_are_valid_and_usable`](../test/features_test.rb#test_struct_classes_are_valid_and_usable), [`test_data_classes_are_usable`](../test/features_test.rb#test_data_classes_are_usable) |
| Enums, constants (also for keyword / invalid names), database labels and MySQL `SET` | [`test_enum_column`](../tests/end2end_pg.rb#test_enum_column), [`test_enum_values_named_like_ruby_keywords_get_valid_constants`](../tests/end2end_pg.rb#test_enum_values_named_like_ruby_keywords_get_valid_constants) | [`test_enum_and_set_columns`](../tests/end2end_mysql2.rb#test_enum_and_set_columns), [`test_enum_values_named_like_ruby_keywords_get_valid_constants`](../tests/end2end_mysql2.rb#test_enum_values_named_like_ruby_keywords_get_valid_constants) | not supported by the SQLite schema | [`test_models_cover_tables_and_enums`](../test/features_test.rb#test_models_cover_tables_and_enums) |
| PostgreSQL arrays, JSON/JSONB, type round trips and RBS | [`test_array_parameters`](../tests/end2end_pg.rb#test_array_parameters), [`test_remaining_postgresql_types_round_trip`](../tests/end2end_pg.rb#test_remaining_postgresql_types_round_trip), [`test_json_and_jsonb_parameters_round_trip_all_json_value_types`](../tests/end2end_pg.rb#test_json_and_jsonb_parameters_round_trip_all_json_value_types) | not PostgreSQL types | not PostgreSQL types | [`test_multi_dimensional_arrays`](../test/features_test.rb#test_multi_dimensional_arrays), [`test_more_postgres_types_composites_and_domains`](../test/features_test.rb#test_more_postgres_types_composites_and_domains) |
| PostgreSQL arrays, JSON/JSONB, type round trips and RBS; a `Time` keeps its instant in a `timestamp` column whatever its offset | [`test_array_parameters`](../tests/end2end_pg.rb#test_array_parameters), [`test_remaining_postgresql_types_round_trip`](../tests/end2end_pg.rb#test_remaining_postgresql_types_round_trip), [`test_timestamp_parameters_keep_their_instant_whatever_their_offset`](../tests/end2end_pg.rb#test_timestamp_parameters_keep_their_instant_whatever_their_offset), [`test_json_and_jsonb_parameters_round_trip_all_json_value_types`](../tests/end2end_pg.rb#test_json_and_jsonb_parameters_round_trip_all_json_value_types) | not PostgreSQL types | not PostgreSQL types | [`test_multi_dimensional_arrays`](../test/features_test.rb#test_multi_dimensional_arrays), [`test_more_postgres_types_composites_and_domains`](../test/features_test.rb#test_more_postgres_types_composites_and_domains) |
| MySQL unsigned integers, BIT, YEAR, DATETIME/TIME (a `Time` keeps its instant in any zone, also in `:copyfrom`), JSON, enums/SET and client quoting | not MySQL | [`test_client_side_quoting_round_trips_supported_values`](../tests/end2end_mysql2.rb#test_client_side_quoting_round_trips_supported_values), [`test_time_values_keep_their_instant_whatever_their_offset`](../tests/end2end_mysql2.rb#test_time_values_keep_their_instant_whatever_their_offset), [`test_time_values_follow_a_utc_database_timezone`](../tests/end2end_mysql2.rb#test_time_values_follow_a_utc_database_timezone), [`test_declared_types_match_runtime_values`](../tests/end2end_mysql2.rb#test_declared_types_match_runtime_values) | not MySQL | [`test_mysql_uses_client_side_binding_and_sqlite_closes_prepared_statements`](../test/features_test.rb#test_mysql_uses_client_side_binding_and_sqlite_closes_prepared_statements) |
| SQLite affinity, JSON/UUID, and NUMERIC integer/float results | not SQLite | not SQLite | [`test_declared_types_match_runtime_values`](../tests/end2end_sqlite3.rb#test_declared_types_match_runtime_values), [`test_declared_types_with_parameters`](../tests/end2end_sqlite3.rb#test_declared_types_with_parameters) | [`test_sqlite_declared_types_follow_type_affinity`](../test/features_test.rb#test_sqlite_declared_types_follow_type_affinity), [`test_sqlite_json_and_uuid_declarations_are_strings`](../test/features_test.rb#test_sqlite_json_and_uuid_declarations_are_strings) |
| SQLite `busy_handler_timeout=` waits for another connection without blocking other threads | not SQLite | not SQLite | [`test_busy_handler_timeout_waits_for_another_connection_without_blocking_threads`](../tests/end2end_sqlite3.rb#test_busy_handler_timeout_waits_for_another_connection_without_blocking_threads) | |
Expand All @@ -41,8 +41,8 @@ CI runs `test/coverage_map_test.rb`, which checks every linked test method exist
| Transaction `tx` used from an `Enumerator`, `Fiber`, `Thread`, `Async` task and nested savepoint; `TransactionEnded` after the transaction | [`test_transaction_query_object_works_from_other_fibers_and_threads`](../tests/end2end_pg.rb#test_transaction_query_object_works_from_other_fibers_and_threads) | [`test_transaction_query_object_works_from_other_fibers_and_threads`](../tests/end2end_mysql2.rb#test_transaction_query_object_works_from_other_fibers_and_threads) | [`test_transaction_query_object_works_from_other_fibers_and_threads`](../tests/end2end_sqlite3.rb#test_transaction_query_object_works_from_other_fibers_and_threads) | |
| PostgreSQL arrays of enums created after the connection was opened | [`test_enum_arrays_of_a_type_created_after_connecting_are_decoded`](../tests/end2end_pg.rb#test_enum_arrays_of_a_type_created_after_connecting_are_decoded) | not MySQL | not SQLite | |
| 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) | |
| `Thread#kill` inside a transaction rolls back; on MySQL a kill during a query replaces the client and ends its transaction | [`test_thread_kill_rolls_back_and_leaves_connection_idle`](../tests/end2end_pg.rb#test_thread_kill_rolls_back_and_leaves_connection_idle) | [`test_thread_kill_rolls_back_and_leaves_connection_outside_transaction`](../tests/end2end_mysql2.rb#test_thread_kill_rolls_back_and_leaves_connection_outside_transaction), [`test_thread_kill_during_a_query_replaces_the_connection_and_ends_its_transaction`](../tests/end2end_mysql2.rb#test_thread_kill_during_a_query_replaces_the_connection_and_ends_its_transaction) | [`test_thread_kill_rolls_back_and_leaves_connection_inactive`](../tests/end2end_sqlite3.rb#test_thread_kill_rolls_back_and_leaves_connection_inactive) | |
| 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) |
| `Thread#kill` inside a transaction rolls back; on PostgreSQL a kill during a query (inside or outside a transaction) cancels it at once and leaves the connection idle; on MySQL a kill during a query replaces the client and ends its transaction | [`test_thread_kill_rolls_back_and_leaves_connection_idle`](../tests/end2end_pg.rb#test_thread_kill_rolls_back_and_leaves_connection_idle), [`test_thread_kill_during_a_query_cancels_it_and_leaves_the_connection_idle`](../tests/end2end_pg.rb#test_thread_kill_during_a_query_cancels_it_and_leaves_the_connection_idle) | [`test_thread_kill_rolls_back_and_leaves_connection_outside_transaction`](../tests/end2end_mysql2.rb#test_thread_kill_rolls_back_and_leaves_connection_outside_transaction), [`test_thread_kill_during_a_query_replaces_the_connection_and_ends_its_transaction`](../tests/end2end_mysql2.rb#test_thread_kill_during_a_query_replaces_the_connection_and_ends_its_transaction) | [`test_thread_kill_rolls_back_and_leaves_connection_inactive`](../tests/end2end_sqlite3.rb#test_thread_kill_rolls_back_and_leaves_connection_inactive) | |
| PostgreSQL statements dropped by `DEALLOCATE ALL` / `DISCARD ALL`, or refused after a column type change (cached plan must not change result type), are prepared again | [`test_statements_are_prepared_again_after_deallocate_all`](../tests/end2end_pg.rb#test_statements_are_prepared_again_after_deallocate_all), [`test_statements_are_prepared_again_after_a_column_type_change`](../tests/end2end_pg.rb#test_statements_are_prepared_again_after_a_column_type_change) | 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) | |
Expand Down
8 changes: 8 additions & 0 deletions examples/pg/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ module ConnectionState
def prepared_statements
if @prepared_statements_pid != backend_pid
@prepared_statements = ::Set[]
@stale_statements = ::Set[]
@prepared_statements_pid = backend_pid
end
@prepared_statements
end

# Statements whose result type changed under them (ALTER TABLE ... TYPE in a migration): deallocated before
# they are prepared again.
def stale_statements
prepared_statements
@stale_statements
end

# Set inside #transaction: a lost connection is then not reconnected until the transaction block ends.
attr_accessor :in_transaction
end
Expand Down
3 changes: 3 additions & 0 deletions examples/pg/connection.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ module PgCodegen
module ConnectionState : _PgConnection
@prepared_statements: ::Set[::String]
@prepared_statements_pid: ::Integer
@stale_statements: ::Set[::String]

def prepared_statements: () -> ::Set[::String]

def stale_statements: () -> ::Set[::String]

attr_accessor in_transaction: bool?
end

Expand Down
Loading
Loading