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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ codec), and it is shipped as a single self-contained WebAssembly module — the
| overrides | a list, see [below](#override-option) | Yes | Overrides the generated (RBS) type of specific columns / parameters. |
| moduleName | default: `<Driver>Codegen` (e.g. `PgCodegen`) | Yes | The Ruby module the generated code lives in, e.g. `MyApp::Db`. |
| preparedStatements | default: `true`<br/>values: `false`,`true` | Yes | `pg` only: `false` sends the SQL with every call (`exec_params`) - needed behind pgbouncer in transaction mode. |
| rename | an object, e.g. `{authors: Writer, bio: biography}` | Yes | Renames generated identifiers: a table name (or `schema.table`) gives the model class, a column name gives the row / model attribute. |
| exactTableNames | default: `false`<br/>values: `false`,`true` | Yes | `true` keeps table names as they are for model classes (`authors` => `Authors`) instead of singularizing them. |

Unknown options are rejected (with a hint for near misses such as `generate_types`). Options can also be set once for
all `sql:` blocks in the top-level `options:` of `sqlc.yaml`, under the plugin's name (`options: {ruby: {moduleName: MyApp::Db}}`).
The `options:` of a codegen entry win over these, and the `overrides` of both apply. A driver used with another
`engine:` (e.g. `pg` with `engine: mysql`) is an error.

Table, column and enum comments (`COMMENT ON ...` in PostgreSQL, `COMMENT '...'` in MySQL) become doc comments of the
generated models (column comments in the RBS).

Unknown option names (e.g. a typo such as `moduleNmae`) and `preparedStatements` with a driver other than `pg` are errors.

Expand All @@ -31,11 +41,18 @@ overrides:
ruby_type:
type: "Symbol"
notNull: true
- column: "*:<column-name>" # every query
- column: "*:<column-name>" # every query and model
ruby_type:
type: "Pathname"
notNull: false
- column: "<table>.<column>" # a table column ("<schema>.<table>.<column>" too): its model and query columns
ruby_type:
type: "Symbol"
- db_type: "uuid" # every column / parameter of a database type
ruby_type:
type: "String"
```
The first match wins, in this order: `<query>:<column>`, `<table>.<column>`, `*:<column>`, `db_type`.

### Configuration
```yaml
Expand Down Expand Up @@ -123,6 +140,8 @@ end
| `:exec` | ✅ | ✅ | ✅ | |
| `:execrows` | ✅ | ✅ | ✅ | returns the number of affected rows |
| `:execlastid` | ✅ | ✅ | ✅ | PostgreSQL / SQLite need a `RETURNING <id>` clause (the value of that column is returned); MySQL returns `client.last_id` |
| `:execresult` | ✅ | ✅ | ✅ | returns `ExecResult` (`rows_affected`, `last_insert_id`; `last_insert_id` is `nil` on PostgreSQL, use `RETURNING`) |
| `:batchexec` / `:batchone` / `:batchmany` | ✅ | ✅ | ✅ | take an `Array` of `<Query>Args` and run the prepared statement once per element on one connection; return `nil` / `Array[Row?]` / `Array[Array[Row]]` |
| `:copyfrom` | ✅ | ✅ | ✅ | takes an `Array` of `<Query>Args` (a generated `Data` class). PostgreSQL: `COPY ... FROM STDIN`. MySQL: `LOAD DATA LOCAL INFILE` from a temporary CSV - needs `local_infile: true` in the `Mysql2::Client` params and `local_infile=1` on the server. SQLite: batched multi-row `INSERT` |

### Macros
Expand All @@ -139,6 +158,8 @@ end
* **Nullability:** nullable columns / parameters are typed `T?` in the `.rbs`.
* **Arrays (PostgreSQL):** `Array[T]` for columns and parameters (`Array[Array[T]]` for `T[][]`); arrays of types without a decoder (`uuid[]`, enum arrays, ...) are `Array[String]`.
* **Names:** a parameter repeated in a query gets a suffix (`id:`, `id_2:`); result columns named like a keyword or a method every row has (`class`, `hash`, `end`, ...) get a trailing `_` (`class_`); a table `authors` next to a table `author` keeps its plural model name (`Authors`). Query names that would replace a generated method (`Transaction`, `Initialize`, ...) are rejected.
* **Other PostgreSQL types:** geometric, range / multirange, full-text, `hstore`, `ltree`, `timetz`, `reg*` and composite types are `String`s (the text the pg gem returns); domains and types the plugin does not know are `untyped` instead of failing the generation.
* **SQLite declared types** (`VARCHAR(255)`, `DECIMAL(10,5)`, `UNSIGNED BIG INT`, ...) follow SQLite's [type affinity](https://www.sqlite.org/datatype3.html#determination_of_column_affinity).
* Types follow the Ruby classes the drivers actually return (e.g. `BigDecimal` for `numeric`/`decimal`, `Date`, `Time`, `IPAddr`; `json`/`jsonb` are parsed, so `untyped`).

Not applicable to the Ruby plugin (they exist in the C# plugin because of its runtime): `withAsyncSuffix` / `withCancellationToken` (Ruby has no async/await: use the Fiber scheduler, see above), `useDapper`, target framework / csproj / namespace options (`moduleName` is the equivalent of the namespace).
Expand Down
2 changes: 1 addition & 1 deletion docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ stdout ◄─ Proto (encode) ◄─ GenerateResponse (query_sql.rb, query_sql.rb
|---|---|
| `exe/plugin.rb` | entry point. Errors go to stderr with exit code 1 (sqlc shows them); `SQLC_GEN_RUBY_DEBUG=1` adds a backtrace |
| `proto.rb`, `plugin_pb.rb` | a ~150 line protobuf wire codec and the subset of sqlc's `codegen.proto` we read. Field numbers must match upstream. `google-protobuf` is a C extension and cannot run inside ruby.wasm |
| `options.rb`, `ruby_version.rb` | parse + validate the `options:` JSON (`driver`, `rubyVersion`, `overrides`, ...) |
| `options.rb`, `ruby_version.rb` | parse + validate the `options:` JSON (`driver`, `rubyVersion`, `overrides`, `rename`, ...; unknown keys are errors) |
| `generator.rb` | the orchestrator: one file per query file (`query_sql.rb`), plus `models.rb`, `connection.rb`, `Gemfile`; builds the module / class / `Data` skeleton |
| `method_gen.rb` | the code of one query: signature (keyword arguments), SQL constant, command (`:one`, `:many`, `:exec`, `:execrows`, `:execlastid`, `:copyfrom`), `sqlc.slice`, bang finder, and the RBS signature |
| `row_layout.rb` | turns result columns into row attributes; `sqlc.embed` collapses a table's columns into one model attribute |
Expand Down
12 changes: 12 additions & 0 deletions examples/authors/mysql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ SELECT * FROM extended_bios WHERE bio_type = ? LIMIT 1;
-- name: TruncateExtendedBios :exec
DELETE FROM extended_bios;

-- name: CreateAuthorResult :execresult
INSERT INTO authors (name, bio) VALUES (?, ?);

-- name: CreateAuthorsBatch :batchexec
INSERT INTO authors (name, bio) VALUES (?, ?);

-- name: GetAuthorBatch :batchone
SELECT * FROM authors WHERE id = ?;

-- name: ListAuthorsByNameBatch :batchmany
SELECT * FROM authors WHERE name = ? ORDER BY id;

-- name: CountRegressionsByText :one
SELECT count(*) FROM regressions WHERE `order` = sqlc.arg(term) OR note = sqlc.arg(term);

Expand Down
22 changes: 22 additions & 0 deletions examples/authors/postgresql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ SELECT * FROM authors
WHERE id > $1 AND id < $2
ORDER BY id;

-- name: InsertSpecialTypes :exec
INSERT INTO postgres_special_types (c_timetz, c_tsvector, c_point, c_int4range, c_oid, c_dimensions, c_positive)
VALUES ($1, $2, $3, $4, $5, $6, $7);

-- name: GetSpecialTypes :one
SELECT * FROM postgres_special_types LIMIT 1;

-- name: TruncateSpecialTypes :exec
TRUNCATE TABLE postgres_special_types;

-- name: CreateAuthorResult :execresult
INSERT INTO authors (name, bio) VALUES ($1, $2);

-- name: CreateAuthorsBatch :batchexec
INSERT INTO authors (name, bio) VALUES ($1, $2);

-- name: GetAuthorBatch :batchone
SELECT * FROM authors WHERE id = $1;

-- name: ListAuthorsByNameBatch :batchmany
SELECT * FROM authors WHERE name = $1 ORDER BY id;

-- name: CreateRegression :one
INSERT INTO regressions (id, "order", payload, doc, bio_types, uids) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;

Expand Down
18 changes: 18 additions & 0 deletions examples/authors/postgresql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ CREATE TABLE postgres_array_types (
c_integer_matrix INTEGER [][]
);

CREATE TYPE extended.dimensions AS (width INTEGER, height INTEGER);

CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);

CREATE TABLE postgres_special_types (
c_timetz TIMETZ,
c_tsvector TSVECTOR,
c_point POINT,
c_int4range INT4RANGE,
c_oid OID,
c_dimensions extended.dimensions,
c_positive positive_int
);

COMMENT ON TABLE authors IS 'People who wrote at least one book.';
COMMENT ON COLUMN authors.bio IS 'Short biography, NULL when unknown.';
COMMENT ON TYPE extended.bio_type IS 'Kinds of biographies.';

CREATE TABLE regressions (
id bigint PRIMARY KEY,
"order" text NOT NULL,
Expand Down
21 changes: 21 additions & 0 deletions examples/authors/sqlite/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ SELECT
FROM authors INNER JOIN books ON authors.id = books.author_id
WHERE books.name = ?;

-- name: CreateAuthorResult :execresult
INSERT INTO authors (name, bio) VALUES (?, ?);

-- name: CreateAuthorsBatch :batchexec
INSERT INTO authors (name, bio) VALUES (?, ?);

-- name: GetAuthorBatch :batchone
SELECT * FROM authors WHERE id = ?;

-- name: ListAuthorsByNameBatch :batchmany
SELECT * FROM authors WHERE name = ? ORDER BY id;

-- name: InsertDeclaredTypes :exec
INSERT INTO declared_types (c_varchar, c_decimal, c_unsigned_big_int, c_native, c_double) VALUES (?, ?, ?, ?, ?);

-- name: GetDeclaredTypes :one
SELECT * FROM declared_types LIMIT 1;

-- name: TruncateDeclaredTypes :exec
DELETE FROM declared_types;

-- name: CreateRegression :exec
INSERT INTO regressions ("order", price) VALUES (?, ?) RETURNING id;

Expand Down
8 changes: 8 additions & 0 deletions examples/authors/sqlite/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ CREATE TABLE books (
FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE
);

CREATE TABLE declared_types (
c_varchar VARCHAR(255),
c_decimal DECIMAL(10, 5),
c_unsigned_big_int UNSIGNED BIG INT,
c_native NATIVE CHARACTER(70),
c_double DOUBLE PRECISION
);

CREATE TABLE regressions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
"order" VARCHAR(255) NOT NULL,
Expand Down
95 changes: 95 additions & 0 deletions examples/mysql2/query_sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,37 @@ class GetFirstExtendedBioByTypeRow < Data.define(:author_name, :name, :bio_type,
DELETE FROM extended_bios
SQL

CREATE_AUTHOR_RESULT_SQL = <<~'SQL'
INSERT INTO authors (name, bio) VALUES (?, ?)
SQL

CREATE_AUTHORS_BATCH_SQL = <<~'SQL'
INSERT INTO authors (name, bio) VALUES (?, ?)
SQL

class CreateAuthorsBatchArgs < Data.define(:name, :bio)
end

GET_AUTHOR_BATCH_SQL = <<~'SQL'
SELECT id, name, bio FROM authors WHERE id = ?
SQL

class GetAuthorBatchRow < Data.define(:id, :name, :bio)
end

class GetAuthorBatchArgs < Data.define(:id)
end

LIST_AUTHORS_BY_NAME_BATCH_SQL = <<~'SQL'
SELECT id, name, bio FROM authors WHERE name = ? ORDER BY id
SQL

class ListAuthorsByNameBatchRow < Data.define(:id, :name, :bio)
end

class ListAuthorsByNameBatchArgs < Data.define(:name)
end

COUNT_REGRESSIONS_BY_TEXT_SQL = <<~'SQL'
SELECT count(*) FROM regressions WHERE `order` = ? OR note = ?
SQL
Expand Down Expand Up @@ -260,6 +291,9 @@ def transaction(isolation: nil)
end
end

class ExecResult < Data.define(:rows_affected, :last_insert_id)
end

# Returns the author with the given id, or nil when there is none.
def get_author(id:)
with_client('get_author') do |client|
Expand Down Expand Up @@ -610,6 +644,67 @@ def truncate_extended_bios
end
end

def create_author_result(name:, bio:)
with_client('create_author_result') do |client|
query_params = [name, bio]
stmt = client.prepare(CREATE_AUTHOR_RESULT_SQL)
begin
stmt.execute(*query_params)
ExecResult.new(stmt.affected_rows, client.last_id)
ensure
stmt.close
end
end
end

def create_authors_batch(rows)
with_client('create_authors_batch') do |client|
stmt = client.prepare(CREATE_AUTHORS_BATCH_SQL)
begin
rows.each do |args|
query_params = [args.name, args.bio]
stmt.execute(*query_params)
end
nil
ensure
stmt.close
end
end
end

def get_author_batch(rows)
with_client('get_author_batch') do |client|
stmt = client.prepare(GET_AUTHOR_BATCH_SQL)
begin
rows.map do |args|
query_params = [args.id]
result = stmt.execute(*query_params, as: :array)
row = result.first
row && GetAuthorBatchRow.new(row[0], row[1], row[2])
end
ensure
stmt.close
end
end
end

def list_authors_by_name_batch(rows)
with_client('list_authors_by_name_batch') do |client|
stmt = client.prepare(LIST_AUTHORS_BY_NAME_BATCH_SQL)
begin
rows.map do |args|
query_params = [args.name]
result = stmt.execute(*query_params, as: :array)
result.map do |row|
ListAuthorsByNameBatchRow.new(row[0], row[1], row[2])
end
end
ensure
stmt.close
end
end
end

def count_regressions_by_text(term:)
with_client('count_regressions_by_text') do |client|
query_params = [term, term]
Expand Down
52 changes: 52 additions & 0 deletions examples/mysql2/query_sql.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,44 @@ module Mysql2Codegen

TRUNCATE_EXTENDED_BIOS_SQL: String

CREATE_AUTHOR_RESULT_SQL: String

CREATE_AUTHORS_BATCH_SQL: String

class CreateAuthorsBatchArgs
attr_reader name: String
attr_reader bio: String?
def initialize: ( String name, String? bio) -> void
end

GET_AUTHOR_BATCH_SQL: String

class GetAuthorBatchRow
attr_reader id: Integer
attr_reader name: String
attr_reader bio: String?
def initialize: ( Integer id, String name, String? bio) -> void
end

class GetAuthorBatchArgs
attr_reader id: Integer
def initialize: ( Integer id) -> void
end

LIST_AUTHORS_BY_NAME_BATCH_SQL: String

class ListAuthorsByNameBatchRow
attr_reader id: Integer
attr_reader name: String
attr_reader bio: String?
def initialize: ( Integer id, String name, String? bio) -> void
end

class ListAuthorsByNameBatchArgs
attr_reader name: String
def initialize: ( String name) -> void
end

COUNT_REGRESSIONS_BY_TEXT_SQL: String

class CountRegressionsByTextRow
Expand Down Expand Up @@ -229,6 +267,12 @@ module Mysql2Codegen

def transaction: [T] (?isolation: (:read_uncommitted | :read_committed | :repeatable_read | :serializable)?) { (self) -> T } -> T

class ExecResult
attr_reader rows_affected: Integer
attr_reader last_insert_id: Integer?
def initialize: ( Integer rows_affected, Integer? last_insert_id) -> void
end

def get_author: (id: Integer) -> GetAuthorRow?

def get_author!: (id: Integer) -> GetAuthorRow
Expand Down Expand Up @@ -277,6 +321,14 @@ module Mysql2Codegen

def truncate_extended_bios: -> void

def create_author_result: (name: String, bio: String?) -> ExecResult

def create_authors_batch: (Array[CreateAuthorsBatchArgs] rows) -> void

def get_author_batch: (Array[GetAuthorBatchArgs] rows) -> Array[GetAuthorBatchRow?]

def list_authors_by_name_batch: (Array[ListAuthorsByNameBatchArgs] rows) -> Array[Array[ListAuthorsByNameBatchRow]]

def count_regressions_by_text: (term: String) -> CountRegressionsByTextRow?

def count_regressions_by_text!: (term: String) -> CountRegressionsByTextRow
Expand Down
Loading
Loading