{"id":48435788,"url":"https://github.com/eikster-dk/sqlc-gen-better-typescript","last_synced_at":"2026-04-06T13:01:01.652Z","repository":{"id":346174776,"uuid":"1137483168","full_name":"eikster-dk/sqlc-gen-better-typescript","owner":"eikster-dk","description":"sqlc.dev plugin that generates typescript code (and Effect v4)","archived":false,"fork":false,"pushed_at":"2026-03-22T17:41:27.000Z","size":156,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-23T07:17:05.915Z","etag":null,"topics":["effect-ts","postgresql","sql","sqlc"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eikster-dk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-19T12:32:09.000Z","updated_at":"2026-03-22T16:46:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/eikster-dk/sqlc-gen-better-typescript","commit_stats":null,"previous_names":["eikster-dk/sqlc-gen-better-typescript"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/eikster-dk/sqlc-gen-better-typescript","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikster-dk%2Fsqlc-gen-better-typescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikster-dk%2Fsqlc-gen-better-typescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikster-dk%2Fsqlc-gen-better-typescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikster-dk%2Fsqlc-gen-better-typescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eikster-dk","download_url":"https://codeload.github.com/eikster-dk/sqlc-gen-better-typescript/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eikster-dk%2Fsqlc-gen-better-typescript/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31473271,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T08:36:52.050Z","status":"ssl_error","status_checked_at":"2026-04-06T08:36:51.267Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["effect-ts","postgresql","sql","sqlc"],"created_at":"2026-04-06T13:00:39.815Z","updated_at":"2026-04-06T13:01:01.637Z","avatar_url":"https://github.com/eikster-dk.png","language":"Go","readme":"# sqlc-gen-better-typescript\n\nA [sqlc](https://sqlc.dev) WASM plugin that generates type-safe TypeScript code from your SQL queries.\n\n## Requirements\n\n- [sqlc](https://sqlc.dev) v1.25.0 or later\n- For the `effect-v4-unstable` builder:\n  - [Effect](https://effect.website) v4 (beta)\n  - TypeScript 5.5+\n\n## What is this?\n\n**sqlc-gen-better-typescript** is a flexible TypeScript code generator for sqlc that supports multiple output formats through a builder architecture. Instead of writing boilerplate database access code, you write SQL and the plugin generates fully typed TypeScript code tailored to your preferred libraries and patterns.\n\nThe current focus is on [Effect v4](https://effect.website) code generation, with planned support for:\n- Native TypeScript (no external dependencies)\n- Zod v4 schema validation\n- Effect v3 compatibility\n\nDepending on the builder you choose, you get:\n\n- **Type-safe parameter schemas** using Effect's Schema library\n- **Type-safe result schemas** with proper null handling via `Option`\n- **Repository services** with Effect's dependency injection via `Layer`\n- **Automatic SQL type mapping** to TypeScript/Effect types\n\n### Effect v4 example\n\nWrite a SQL query:\n\n```sql\n-- name: GetCustomer :one\nSELECT id, email, name, phone, created_at\nFROM customers\nWHERE id = $1;\n```\n\nGet a fully typed Effect repository:\n\n```typescript\n// Generated automatically\nexport const GetCustomerParams = Schema.Struct({\n  id: Schema.Int,\n})\n\nexport type GetCustomerParams = typeof GetCustomerParams.Type\n\nexport const GetCustomerResult = Schema.Struct({\n  id: Schema.Int,\n  email: Schema.String,\n  name: Schema.String,\n  phone: Schema.OptionFromNullOr(Schema.String),\n  created_at: Schema.Date,\n})\n\nexport type GetCustomerResult = typeof GetCustomerResult.Type\n\n// Repository interface\nexport interface CustomersRepositoryShape {\n  readonly getCustomer: (params: GetCustomerParams) =\u003e Effect.Effect\u003c\n    Option.Option\u003cGetCustomerResult\u003e,\n    SqlError.SqlError | Schema.SchemaError\n  \u003e\n}\n\n// Service Tag\nexport class CustomersRepository extends ServiceMap.Service\u003c\n  CustomersRepository,\n  CustomersRepositoryShape\n\u003e()(\"CustomersRepository\") {}\n\n// Implementation\nconst customersRepositoryImpl = Effect.gen(function* () {\n  const sql = yield* SqlClient.SqlClient\n\n  const getCustomer = SqlSchema.findOneOption({\n    Request: GetCustomerParams,\n    Result: GetCustomerResult,\n    execute: (params) =\u003e sql.unsafe(\n      `SELECT id, email, name, phone, created_at FROM customers WHERE id = $1`,\n      [params.id]\n    )\n  })\n\n  return { getCustomer } satisfies CustomersRepositoryShape\n})\n\n// Live Layer\nexport const customersRepositoryLive = Layer.effect(CustomersRepository, customersRepositoryImpl)\n\n// Usage\nconst program = Effect.gen(function* () {\n  const repo = yield* CustomersRepository\n  const customer = yield* repo.getCustomer({ id: 1 })\n  // customer is Option.Option\u003cGetCustomerResult\u003e\n})\n```\n\n### Nested Results with `sqlc.embed`\n\nUse `sqlc.embed` to group columns from joined tables into nested structures. This is useful for queries that join multiple tables and you want the result to reflect that structure.\n\nWrite a SQL query with embeds:\n\n```sql\n-- name: GetOrderWithCustomer :one\nSELECT sqlc.embed(orders), sqlc.embed(customers)\nFROM orders\nJOIN customers ON orders.customer_id = customers.id\nWHERE orders.id = $1;\n```\n\nGet a nested result type:\n\n```typescript\n// Generated automatically - Row schema represents flat database result\nconst GetOrderWithCustomerRow = Schema.Struct({\n  orders_id: Schema.Int,\n  orders_customer_id: Schema.Int,\n  orders_status: OrderStatusSchema,\n  orders_total_cents: Schema.Int,\n  orders_shipping_address: Schema.NullOr(Schema.String),\n  // ... more orders columns\n  customers_id: Schema.Int,\n  customers_email: Schema.String,\n  customers_name: Schema.String,\n  // ... more customers columns\n})\n\n// Nested embed schemas with proper Option handling\nconst OrderEmbed = Schema.Struct({\n  id: Schema.Int,\n  customer_id: Schema.Int,\n  status: OrderStatusSchema,  // Enums are preserved\n  total_cents: Schema.Int,\n  shipping_address: Schema.OptionFromNullOr(Schema.String),  // Nullable -\u003e Option\n  // ...\n})\n\nconst CustomerEmbed = Schema.Struct({\n  id: Schema.Int,\n  email: Schema.String,\n  name: Schema.String,\n  // ...\n})\n\n// Result schema transforms flat rows to nested structure\nexport const GetOrderWithCustomerResult = GetOrderWithCustomerRow.pipe(\n  Schema.decodeTo(\n    Schema.Struct({\n      order: OrderEmbed,      // Singularized table name\n      customer: CustomerEmbed,\n    }),\n    SchemaTransformation.transform({\n      decode: (row) =\u003e ({\n        order: {\n          id: row.orders_id,\n          customer_id: row.orders_customer_id,\n          status: row.orders_status,\n          shipping_address: row.orders_shipping_address,\n          // ...\n        },\n        customer: {\n          id: row.customers_id,\n          email: row.customers_email,\n          name: row.customers_name,\n          // ...\n        },\n      }),\n      encode: () =\u003e { throw new Error(\"Encode not supported for sqlc.embed queries\"); },\n    })\n  )\n)\n\n// Result type is nested:\n// {\n//   order: { id: number, status: \"pending\" | \"shipped\" | ..., shipping_address: Option\u003cstring\u003e, ... }\n//   customer: { id: number, email: string, name: string, ... }\n// }\n```\n\n**Key features:**\n- Table names are singularized for field names (`orders` → `order`, `customers` → `customer`)\n- Columns are prefixed with table name to avoid conflicts (`orders_id`, `customers_id`)\n- Enum types are preserved in embed schemas\n- Nullable fields use `Schema.OptionFromNullOr` for consistent API with non-embed queries\n- The transformation is decode-only (embed queries are read-only)\n\n## Builders\n\nThe plugin uses a **builder** architecture to support different code generation targets. Each builder produces output tailored for a specific framework or library version.\n\n## Available Builders\n\n| Builder | Description | Status |\n|---------|-------------|--------|\n| `effect-v4-unstable` | Generates Effect v4 TypeScript code using `effect/unstable/sql` | Available |\n\n### Effect v4 Builder\n\nThe `effect-v4-unstable` builder generates idiomatic Effect v4 code using the `effect/unstable/sql` module.\n\n#### SQL Generation\n\nBy default, the plugin transforms sqlc's parameterized SQL into Effect's tagged template literal syntax:\n\n```typescript\n// Default output (template literals)\n// GetCustomer\n// SELECT * FROM customers WHERE id = $1 AND email = $2\nexecute: (params) =\u003e sql`SELECT * FROM customers WHERE id = ${params.id} AND email = ${params.email}`\n```\n\nThe original SQL query is included as a comment above each query implementation for reference.\n\n#### Preserving Original SQL\n\nIf you prefer to keep the sqlc-generated SQL statements unmodified, you can disable template literal transformation:\n\n```yaml\noptions:\n  builder: effect-v4-unstable\n  disable_template_literals: true\n```\n\nThis uses `sql.unsafe()` which passes the SQL exactly as sqlc generated it:\n\n```typescript\n// With disable_template_literals: true\n// GetCustomer\n// SELECT * FROM customers WHERE id = $1 AND email = $2\nexecute: (params) =\u003e sql.unsafe(\n  `SELECT * FROM customers WHERE id = $1 AND email = $2`,\n  [params.id, params.email]\n)\n```\n\nBoth approaches are equally safe from SQL injection. The choice is between:\n- **Template literals (default)**: Cleaner syntax, but transforms the SQL by replacing `$1`, `$2` placeholders with interpolated parameters\n- **sql.unsafe()**: Preserves the original sqlc-generated SQL without modification\n\n\u003e **Note:** In both cases, the plugin may still modify SQL to handle edge cases like duplicate column names (e.g., `id` becomes `id`, `id_2`, `id_3`).\n\n#### Repository Pattern\n\nEach SQL file in your `queries/` directory becomes its own encapsulated repository. For example:\n\n```\nqueries/\n├── customers.sql    → CustomersRepository.ts\n├── orders.sql       → OrdersRepository.ts\n└── products.sql     → ProductsRepository.ts\n```\n\nAll queries defined in a SQL file are grouped into a single repository service. This keeps related database operations together and provides clean dependency injection through Effect's `Layer` system.\n\n#### Generated Output\n\nFor each repository, the builder generates:\n\n- **Parameter schemas** - Type-safe input validation for each query\n- **Result schemas** - Type-safe output parsing with proper null handling (`Option`)\n- **Repository interface** - A typed interface defining all available operations\n- **Service tag** - An Effect service tag for dependency injection\n- **Live implementation** - The actual repository implementation using `SqlClient`\n- **Layer export** - A ready-to-use `Layer` for providing the repository\n\n#### Usage\n\n```typescript\nimport { CustomersRepository, customersRepositoryLive } from \"./repositories/CustomersRepository\"\nimport { Effect, Layer } from \"effect\"\nimport { PgClient } from \"effect/unstable/sql/PgClient\"\n\nconst program = Effect.gen(function* () {\n  const repo = yield* CustomersRepository\n  \n  // All queries from customers.sql are available as methods\n  const customer = yield* repo.getCustomer({ id: 1 })\n  const allCustomers = yield* repo.listCustomers()\n  yield* repo.createCustomer({ email: \"new@example.com\", name: \"New Customer\" })\n})\n\n// Provide the repository layer (requires SqlClient)\nconst runnable = program.pipe(\n  Effect.provide(customersRepositoryLive),\n  Effect.provide(/* your PgClient layer */)\n)\n```\n\n#### Supported sqlc Commands\n\n| Command | Supported | Effect Return Type | Description |\n|---------|-----------|-------------------|-------------|\n| `:one` | Yes | `Option.Option\u003cResult\u003e` | Returns at most one row |\n| `:many` | Yes | `Result[]` | Returns zero or more rows |\n| `:exec` | Yes | `void` | Executes without returning data |\n| `:execrows` | Yes | `number` | Returns the number of affected rows |\n| `:execresult` | No | - | Not yet implemented |\n| `:copyfrom` | No | - | Not yet implemented |\n| `:batchexec` | No | - | Not yet implemented |\n| `:batchone` | No | - | Not yet implemented |\n| `:batchmany` | No | - | Not yet implemented |\n\n#### Supported sqlc Macros\n\n| Macro | Supported | Description |\n|-------|-----------|-------------|\n| `sqlc.arg('name')` | Yes | Explicit parameter naming |\n| `sqlc.narg('name')` | No | Nullable argument - not yet implemented |\n| `sqlc.slice('name')` | No | Slice expansion - use `= ANY($1::type[])` instead (see below) |\n| `sqlc.embed(table)` | Yes | Embed table columns into nested structures |\n\n\u003e **Note on `sqlc.slice`:** While `sqlc.slice()` is not supported, you can achieve the same result using PostgreSQL's `ANY` operator with array casting:\n\u003e ```sql\n\u003e -- Instead of: WHERE id IN (sqlc.slice('ids'))\n\u003e -- Use:\n\u003e WHERE id = ANY($1::int[])\n\u003e ```\n\u003e This generates a parameter typed as `Schema.Array(Schema.Int)` and works correctly with PostgreSQL.\n\n### Future Builders (Planned)\n\n| Builder | Description |\n|---------|-------------|\n| `effect-v3` | Effect v3 compatible code generation |\n| `typescript` | Plain TypeScript with no Effect dependency |\n| `zod-v4` | TypeScript with Zod v4 schemas for validation |\n\n## Supported Database Engines\n\n| Engine | Supported |\n|--------|-----------|\n| PostgreSQL | Yes |\n| MySQL | No |\n| SQLite | No |\n\n## Type Mapping\n\nThe following table shows how PostgreSQL types are mapped to Effect Schema types:\n\n| PostgreSQL Type | Effect Schema | Notes |\n|-----------------|---------------|-------|\n| `integer`, `int`, `int4`, `serial` | `Schema.Int` | |\n| `bigint`, `int8`, `bigserial` | `BigIntFromString` | PostgreSQL returns bigint as string to preserve precision |\n| `smallint`, `int2`, `smallserial` | `Schema.Int` | |\n| `real`, `float4`, `double precision`, `float8` | `Schema.Number` | |\n| `numeric`, `money` | `Schema.String` | Preserves precision |\n| `boolean`, `bool` | `Schema.Boolean` | |\n| `text`, `varchar`, `char`, `citext` | `Schema.String` | |\n| `uuid` | `Schema.String` | |\n| `date` | `Schema.Date` | |\n| `timestamp`, `timestamptz` | `Schema.Date` | |\n| `time`, `timetz`, `interval` | `Schema.String` | |\n| `json`, `jsonb` | `Schema.Unknown` | |\n| `bytea` | `Schema.Uint8Array` | |\n| `inet`, `cidr`, `macaddr` | `Schema.String` | |\n| Arrays (e.g., `int[]`) | `Schema.Array(...)` | Wraps the base type |\n| Enums | `Schema.Literals([...])` | Generated from enum definition |\n\n### Nullability\n\n- **Parameters**: Nullable parameters use `Schema.optional()`, allowing callers to omit the field\n- **Results**: Nullable results use `Schema.OptionFromNullOr()`, transforming `null` to `Option.None`\n\n## Configuration\n\nConfigure the plugin in your `sqlc.yaml`:\n\n```yaml\nversion: '2'\nplugins:\n- name: better-typescript\n  wasm:\n    url: https://github.com/eikster-dk/sqlc-gen-better-typescript/releases/download/v[version]/plugin.wasm\n    sha256: [calculatedSha]\n\nsql:\n- schema: schema/\n  queries: queries/\n  engine: postgresql\n  codegen:\n  - out: src/repositories\n    plugin: better-typescript\n    options:\n      builder: effect-v4-unstable\n      # debug: true\n      # debug_dir: debug\n```\n\n### Plugin Options\n\n| Option | Type | Required | Default | Description |\n|--------|------|----------|---------|-------------|\n| `builder` | string | Yes | - | The code generation builder to use. Must be one of the available builders (e.g., `effect-v4-unstable`). |\n| `disable_template_literals` | boolean | No | `false` | Preserve original sqlc SQL using `sql.unsafe()` instead of transforming to template literals. See [Preserving Original SQL](#preserving-original-sql). |\n| `import_extension` | string | No | `\"\"` | Explicit extension for generated relative imports. Allowed: `\"\"`, `.js`, `.ts`. Use `.js` for Node ESM (`moduleResolution: nodenext`/`node16`). |\n| `debug` | boolean | No | `false` | Enable debug mode to output intermediate representations and detailed logs during code generation. |\n| `debug_dir` | string | No | `\"debug\"` | Directory where debug output files are written when debug mode is enabled. |\n\n## Getting Started\n\n1. Install sqlc: https://docs.sqlc.dev/en/latest/overview/install.html\n\n2. Create your `sqlc.yaml` configuration (see above)\n\n3. Write your SQL schema and queries\n\n4. Run sqlc:\n   ```bash\n   sqlc generate\n   ```\n\n5. Use the generated repositories in your Effect application\n\n## Development\n\n### Building the Plugin\n\n```bash\nmake build\n```\n\n### Running Tests\n\n```bash\nmake test\n```\n\n### Project Structure\n\n```\n.\n├── cmd/plugin/           # Plugin source code\n│   ├── main.go           # Entry point\n│   └── internal/\n│       ├── builders/     # Code generation builders\n│       ├── config/       # Plugin configuration\n│       ├── mapper/       # sqlc to internal type mapping\n│       ├── models/       # Internal data models\n│       └── logger/       # Structured logging\n├── examples/             # Example projects\n│   └── effect-v4/        # Effect v4 example\n└── dist/                 # Built plugin artifacts\n```\n\n## License\n\nSee [LICENSE](LICENSE) file.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feikster-dk%2Fsqlc-gen-better-typescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feikster-dk%2Fsqlc-gen-better-typescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feikster-dk%2Fsqlc-gen-better-typescript/lists"}