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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
184 changes: 184 additions & 0 deletions .claude/add-a-dialect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Adding a new database dialect

A walkthrough of every file you need to touch, in roughly the order you should touch them. The
SQLite support PR is the cleanest recent reference — every step below points at a SQLite analogue.

## 0. What the upstream `foundations-jdbc` library must already provide

`typr` is the code generator; the runtime types and codecs live in the external
`dev.typr.foundations:foundations-jdbc` library. Before you start, make sure that library exposes:

- A typed types catalogue, e.g. `dev.typr.foundations.XxxTypes` with one entry per declarable type
(integer/varchar/decimal/date/…). Each entry is a `XxxType<JvmType>` carrying the read/write
codec and JSON codec.
- `dev.typr.foundations.connect.XxxConfig` for building a JDBC connection, and
`DatabaseKind.XXX` enum value.
- Scala and Kotlin wrappers: `dev.typr.foundationssc.XxxTypes` and
`dev.typr.foundationskt.XxxTypes`.

If any of these are missing, send the PR to foundations first.

## 1. Add a `Dialect` entry

File: `typr-dsl/src/java/dev/typr/dsl/Dialect.java` — the DSL's per-dialect SQL grammar bits
(identifier quoting, casts, null-safe comparison, LIMIT/OFFSET, tuple-IN support).

The `Dialect` interface has reasonable defaults for everything except `bigint()`, `quoteIdent()`,
`escapeIdent()`, `typeCast()`, `columnRef()`, `nullSafeEquals()`, `nullSafeNotEquals()` — override
just what differs from the default (PostgreSQL-shaped) behaviour.

Re-export it in the Scala wrapper at
`typr-dsl-scala/src/scala/dev/typr/dslsc/package.scala`'s `object Dialect`.

(Kotlin can use the Java static field directly — no wrapper needed.)

## 2. Define the `db.XxxType` ADT

File: `typr-codegen/src/scala/typr/db.scala`.

Add a `sealed trait XxxType extends Type` plus one `case object` (or `case class` for parameterised
types like `Decimal(precision, scale)`) per concrete declarable type. Add `XxxType` to the
`Unknown` mixin trait list at the bottom — that's the fallback for unrecognised JDBC type names.

## 3. Add `DbType.Xxx` + connection plumbing

Files:

- `typr-codegen/src/scala/typr/DbType.scala` — add `case object Xxx extends DbType` returning your
adapter, plus a branch in `detect(...)` and `detectFromDriver(...)`.
- `typr-codegen/src/scala/typr/TypoDataSource.scala` — add a `hikariXxx*(...)` constructor and a
`DatabaseKind.XXX` branch in `hikari(...)`.

## 4. Write the codegen adapter

File: `typr-codegen/src/scala/typr/internal/codegen/XxxAdapter.scala`.

Mirror `DuckDbAdapter` (closest in spirit for most embedded DBs) or `Db2Adapter` (single-schema,
no native arrays). Five layers:

1. **SQL syntax** — `quoteIdent`, `typeCast`, the various `columnReadCast`/`columnWriteCast` (most
dialects can return `Code.Empty`).
2. **Runtime types** — point at `XxxTypes` / `XxxType` / `XxxText` and pick a `typeFieldName`
(e.g. `xxxType`).
3. **Capabilities** — `supportsArrays`, `supportsReturning`, `supportsCopyStreaming`, the upsert
strategy.
4. **SQL templates** — upsert (`ON CONFLICT` vs `MERGE`), conflict update clause, returning clause.
5. **Schema DDL** — `dropSchemaDdl` / `createSchemaDdl`. For dialects without schemas (SQLite,
embedded DBs), return a SQL comment.

## 5. Write the metadata-extraction package

Directory: `typr-codegen/src/scala/typr/internal/xxx/`. Four files:

- `XxxJdbcMetadata.scala` — wraps `ResultSetMetaData` into `MetadataColumn`. This is essentially
identical across dialects — copy `DuckDbJdbcMetadata` and rename.
- `XxxTypeMapperDb.scala` — maps the declared/JDBC type name string to a `db.XxxType`. Read your
database's reference for type aliases; many dialects accept synonyms (BIGINT/INT8/LONG/…).
- `XxxSqlFileMetadata.scala` — analyses user-supplied `.sql` files using sqlglot. Copy
`DuckDbSqlFileMetadata`; the only dialect-specific bits are (a) how you read the schema (the
catalogue queries) and (b) the sqlglot `dialect` string.
- `XxxMetaDb.scala` — reads tables/columns/PKs/FKs/uniques/views from the database catalogue.
Look at how your DB exposes metadata (information_schema, system catalog views, or PRAGMAs) and
shape the queries to fit. Return a `MetaDb(dbType, relations, enums = Nil, domains = Nil, …)`.

## 6. Wire all dispatch sites

These are the files that have one `case DbType.X => …` arm per dialect. Each new dialect needs an
arm added everywhere:

- `typr-codegen/src/scala/typr/MetaDb.scala` — both the `typeMapperDb` match and the `fromDb`
match.
- `typr-codegen/src/scala/typr/internal/InstanceRequirements.scala` — the heuristic name guess
and the `dbTypeFieldNameFor` map.
- `typr-codegen/src/scala/typr/internal/generate.scala` — the `databaseName` string used as a
TypeDefinition discriminator, **plus** the precision-types case list near the bottom that emits
`PreciseConstraint.*` for `VarChar(Some(n))` etc.
- `typr-codegen/src/scala/typr/internal/codegen/DbLibFoundations.scala` — selectByIds /
deleteByIds bodies. If your DB has no native arrays (most non-Postgres do), fold it into the
existing `case DbType.SqlServer | DbType.DB2 | …` arms rather than copy-pasting.
- `typr-codegen/src/scala/typr/internal/sqlfiles/SqlFileReader.scala` — dispatch to your
`XxxSqlFileMetadata`.
- `typr-codegen/src/scala/typr/internal/TypeMapperJvmNew.scala` — both the `baseType` and the
precise-types match. (`TypeMapperJvmOld` is PostgreSQL-only legacy; skip.)
- `typr-codegen/src/scala/typr/internal/TypeMatcher.scala` — `typeName` (for the matcher's
per-column name string).
- `typr-codegen/src/scala/typr/internal/TypeCompatibilityChecker.scala` — add a `CompatibilityClass`
arm for every Java-equivalent class (String/Boolean/Int/Long/…) so cross-dialect Bridge type
matching works.
- `typr-codegen/src/scala/typr/internal/ComputedTestInserts.scala` — the `case`s for max-length
detection on text columns.
- `typr/src/scala/typr/bridge/TypeSuggester.scala` — group types into the "text/integer/numeric/
boolean/temporal/uuid/json" buckets the bridge UI uses.

## 7. CLI wiring

- `typr-config.schema.json` — add `"xxx"` to the boundary `type` enum and add a `xxxBoundary`
definition (use `duckdbBoundary` as a template for embedded DBs, or `databaseBoundary` for
server-based ones).
- Run `bleep run generate-config-types` to regenerate the typed config classes
(`XxxBoundary.scala` shows up under `typr-codegen/generated-and-checked-in-jsonschema/`).
- `typr/src/scala/typr/cli/config/ConfigParser.scala` — add `Some("xxx") => …` arms for source
*and* boundary parsing, and add `ParsedSource.Xxx` / `ParsedBoundary.Xxx` cases.
- `typr/src/scala/typr/cli/config/ConfigToOptions.scala` — `convertXxxBoundary` + `convertXxxSource`.
- `typr/src/scala/typr/cli/commands/Generate.scala` — `fetchXxxBoundary`, `fetchXxxSource`,
`generateXxxForOutput`, plus the two dispatch sites in `runTwoPhaseGeneration`. Watch for any
driver-specific quirks loading the schema (SQLite's xerial driver, for example, can't run
multi-statement strings in a single `execute()` — the SQLite path splits on `;` first).
- `typr/src/scala/typr/cli/app/MetaDbFetch.scala` — add a `ParsedSource.Xxx` arm + `fetchXxx`.
- `typr/src/scala/typr/cli/app/ConnectionTest.scala` — add a `ParsedSource.Xxx` arm + `tryXxx`,
and include `Xxx` in `isTestable`'s pattern.
- `typr/src/scala/typr/cli/app/LoadedSource.scala` — include `ParsedSource.Xxx` in the database
pattern arm.
- The TUI screens (`SchemaPicker`, `SourceForm`, `SourceList`, `MainMenu`) — fold the new kind
string into the existing `"duckdb" | "xxx"` cases for path-style sources, or use the database
patterns for host/port sources.

## 8. Build wiring

- `bleep.yaml` — add three tester project entries (`testers/xxx/java`, `testers/xxx/kotlin`,
`testers/xxx/scala`), each with the right JDBC driver dependency. Also add the driver to the
`typr-codegen` project's dependencies so the CLI can connect.
- `typr.yaml` — add a boundary entry (with `schema_sql`, `sql_scripts`, `path` or host/port) and
three outputs (`xxx-java`, `xxx-kotlin`, `xxx-scala`).

## 9. Test data + scripts

- `sql-init/xxx/00-schema.sql` — exercise every column type your `db.XxxType` ADT models, plus
composite PK, composite FK, UNIQUE, views, and `precision_types[_null]` for precise-type
generation.
- `sql-scripts/xxx/*.sql` — half a dozen parameterised queries covering SELECT, INSERT-with-
RETURNING, UPDATE, DELETE, JOIN.

## 10. Testers

Under `testers/xxx/{java,kotlin,scala}` add a `src/{lang}/testdb/XxxTestHelper` and a
`BasicCrudTest` that exercises the generated repos. Mirror `testers/duckdb/{java,kotlin,scala}` —
if your driver supports multi-statement `execute()`, you can use `connectionInitSql(schema)`
directly; otherwise split the schema yourself (SQLite shows this pattern).

## 11. Generate, fmt, test

```bash
bleep run typr -- generate --source xxx --accept
bleep fmt
bleep test testers/xxx
```

## Things that bit me on SQLite specifically

- **Single connection, in-memory.** `:memory:` databases live inside one JDBC connection; opening
a second connection gives you an empty DB. Foundations' `singleConnectionMode()` handles the
reuse but `connectionInitSql` runs once via a *single* `stmt.execute(...)` — for drivers that
don't accept multi-statement strings (xerial SQLite is one) the schema only partially loads.
The fix is to bypass `connectionInitSql` and run statements one at a time during a one-shot
init, then switch the transactor to `rollbackOnly()` for tests.
- **No schemas.** Force `SchemaMode.SingleSchema("main")` in your `convertXxxBoundary`, and emit
`RelationName(None, name)` in metadata. SQLite's `main` namespace isn't really a schema.
- **Type affinity, not declared type.** SQLite stores values in five storage classes regardless
of column declaration — your `XxxTypeMapperDb` needs to match common synonyms (BIGINT, INT8,
INT2, VARCHAR(n), CLOB, …) to a specific `db.XxxType` so the codecs round-trip. The full
affinity-substring fallback at the bottom of `SqliteTypeMapperDb` handles "anything goes" cases
(`VARYING CHARACTER`, `DOUBLE PRECISION`, etc.).
- **Foreign keys off by default.** SQLite needs `PRAGMA foreign_keys = ON` per connection. The
foundations `SqliteConfig.Builder.foreignKeys(true)` sets it via a driver property, but if
you're opening raw JDBC for tests, set it yourself.
19 changes: 16 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ Typr is a database code generator that creates type-safe JVM code from database
- **DuckDB** - embedded analytical database
- **SQL Server** - T-SQL specific features
- **Oracle** - including OBJECT and MULTISET types
- **DB2** - including distinct types
- **SQLite** - embedded, type-affinity model with foundations SqliteTypes aliases

When adding a new dialect, follow [`.claude/add-a-dialect.md`](.claude/add-a-dialect.md) — a touch-point checklist distilled from the SQLite work.

## Foundations JDBC

`foundations-jdbc` is a standalone JDBC wrapper library with perfect type modeling for all supported databases. See `site/docs-jdbc/` for full documentation.
`foundations-jdbc` is a standalone JDBC wrapper library with perfect type modeling for all supported databases. It lives in its own repository (`dev.typr.foundations:foundations-jdbc`, currently RC6) — typr depends on it as a Maven artifact, not as a submodule. Its docs ship from that repo, not from this one.

## Build System

Expand Down Expand Up @@ -143,6 +147,7 @@ All databases use `sql-init/{database}/` for schema initialization, mounted to `
- **SQL Server**: `sql-init/sqlserver/` - Custom entrypoint `00-entrypoint.sh` starts server and runs SQL
- **DB2**: `sql-init/db2/` - Shell script `00-run-sql.sh` runs SQL files
- **DuckDB**: `sql-init/duckdb/` - Loaded by generation script (embedded database, no docker)
- **SQLite**: `sql-init/sqlite/` - Loaded by generation script (embedded database, no docker)

### Ensuring Databases Are Up to Date

Expand Down Expand Up @@ -296,6 +301,8 @@ typr/ # Main code generator
│ ├── mariadb/ # MariaDB adapter
│ ├── oracle/ # Oracle adapter
│ ├── duckdb/ # DuckDB adapter
│ ├── db2/ # DB2 adapter
│ ├── sqlite/ # SQLite adapter
│ └── sqlserver/ # SQL Server adapter
│ └── openapi/ # OpenAPI code generation

Expand All @@ -308,6 +315,8 @@ testers/ # Integration test projects
├── duckdb/ # DuckDB testers (java, kotlin, scala)
├── oracle/ # Oracle testers (java, kotlin, scala)
├── sqlserver/ # SQL Server testers (java, kotlin, scala)
├── db2/ # DB2 testers (java, kotlin, scala)
├── sqlite/ # SQLite testers (java, kotlin, scala)
└── openapi/ # OpenAPI framework testers
├── java/ # JAX-RS, Spring, Quarkus
├── kotlin/ # JAX-RS, Spring, Quarkus
Expand All @@ -332,14 +341,18 @@ sql-init/ # Schema files (mounted to Docker)
├── mariadb/ # MariaDB schemas
├── oracle/ # Oracle schemas
├── sqlserver/ # SQL Server schemas
└── duckdb/ # DuckDB schemas (loaded by script)
├── duckdb/ # DuckDB schemas (loaded by script)
├── db2/ # DB2 schemas
└── sqlite/ # SQLite schemas (loaded by script)

sql-scripts/ # SQL query files for code generation
├── postgres/ # PostgreSQL SQL queries
├── mariadb/ # MariaDB SQL queries
├── sqlserver/ # SQL Server SQL queries
├── oracle/ # Oracle SQL queries
└── duckdb/ # DuckDB SQL queries
├── duckdb/ # DuckDB SQL queries
├── db2/ # DB2 SQL queries
└── sqlite/ # SQLite SQL queries

typr-internal-sql/ # Internal SQL for Typr codegen
```
Expand Down
37 changes: 37 additions & 0 deletions bleep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,42 @@ projects:
sources:
- ./generated-and-checked-in
- ./src/scala
testers/sqlite/java:
dependencies:
- com.fasterxml.jackson.core:jackson-annotations:2.17.2
- com.fasterxml.jackson.core:jackson-databind:2.17.2
- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.2
- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2
- com.github.sbt:junit-interface:0.13.3
- junit:junit:4.13.2
- org.xerial:sqlite-jdbc:3.46.1.3
dependsOn: typr-dsl
isTestProject: true
java:
options: -proc:none
platform:
name: jvm
sources:
- ./generated-and-checked-in
- ./src/java
testers/sqlite/kotlin:
dependencies: org.xerial:sqlite-jdbc:3.46.1.3
extends: template-kotlin-db-tester
testers/sqlite/scala:
dependencies:
- com.fasterxml.jackson.core:jackson-annotations:2.17.2
- com.fasterxml.jackson.core:jackson-databind:2.17.2
- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.17.2
- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2
- com.github.sbt:junit-interface:0.13.3
- junit:junit:4.13.2
- org.xerial:sqlite-jdbc:3.46.1.3
dependsOn: typr-dsl-scala
extends: template-scala-3
isTestProject: true
sources:
- ./generated-and-checked-in
- ./src/scala
testers/duckdb/java:
dependencies:
- com.fasterxml.jackson.core:jackson-annotations:2.17.2
Expand Down Expand Up @@ -937,6 +973,7 @@ projects:
- org.playframework.anorm::anorm:2.7.0
- org.postgresql:postgresql:42.7.3
- org.slf4j:slf4j-nop:2.0.13
- org.xerial:sqlite-jdbc:3.46.1.3
dependsOn: typr-dsl-scala
extends:
- template-scala-3
Expand Down
3 changes: 2 additions & 1 deletion site/docs-typr/boundaries/databases/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The compiler checks your application code. But at the database boundary, you're

Typr DB treats your database schema as the source of truth:

1. **Read your schema** from PostgreSQL, MariaDB, Oracle, SQL Server, DuckDB, or DB2
1. **Read your schema** from PostgreSQL, MariaDB, Oracle, SQL Server, DuckDB, DB2, or SQLite
2. **Generate typed code** for every table, view, and relationship
3. **Enforce at compile time** that all database access uses the correct types

Expand Down Expand Up @@ -57,6 +57,7 @@ When you change a column, rename a table, or modify a relationship—the compile
| SQL Server | Full support |
| DuckDB | Full support |
| IBM DB2 | Full support |
| SQLite | Full support |

## Supported Languages

Expand Down
6 changes: 6 additions & 0 deletions site/docs-typr/boundaries/databases/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ For complete installation and configuration instructions, see the [Getting Start
| Oracle | Full support including OBJECT and MULTISET types |
| DuckDB | Full support for embedded analytical workloads |
| IBM DB2 | Full support including distinct types |
| SQLite | Full support for embedded workloads — every type affinity, RETURNING, ON CONFLICT upserts |

:::tip SQLite foreign keys
SQLite parses `REFERENCES` clauses but doesn't enforce them unless `PRAGMA foreign_keys = ON` is set on each connection. The `SqliteConfig.Builder.foreignKeys(true)` helper writes this as a driver property so every pooled connection inherits it — turn it on explicitly if you're building a `SqliteConfig` by hand.
:::

### Configuration Example

Expand Down Expand Up @@ -54,6 +59,7 @@ Each database has unique features that Typr models with full fidelity:
- **Oracle**: OBJECT types, nested tables, MULTISET
- **SQL Server**: Alias types, table-valued parameters
- **DuckDB**: Nested types, structs, lists
- **SQLite**: Type-affinity model with aliases (BIGINT/INT8/VARCHAR/CLOB...), ISO-8601 date/time as TEXT, no schemas

See [Type Safety](/typr/boundaries/databases/type-safety/id-types) for details on how these are represented in generated code.

Expand Down
1 change: 1 addition & 0 deletions site/docs-typr/boundaries/databases/type-safety/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Typr provides full support for array and list types across databases that suppor
| Oracle | VARRAY, Nested Table | Via collection types |
| SQL Server | - | No array support |
| DB2 | - | No array support |
| SQLite | - | No array support |

## PostgreSQL Arrays

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Some databases support collection types for storing multiple values in a single
| SQL Server | Table-valued parameters | Procedure parameters only |
| MariaDB/MySQL | JSON arrays | No native arrays |
| DB2 | - | No native collections |
| SQLite | - | No native collections |

## PostgreSQL Arrays

Expand Down Expand Up @@ -97,7 +98,7 @@ Currently mapped to String for flexibility. Native Map support is planned.

## Databases Without Collection Support

MariaDB/MySQL, SQL Server (outside of table types), and DB2 don't have native array types. Alternatives:
MariaDB/MySQL, SQL Server (outside of table types), DB2, and SQLite don't have native array types. Alternatives:
- **JSON arrays** - Flexible but less type-safe
- **Junction tables** - For many-to-many relationships
- **Table-valued parameters** (SQL Server) - For procedure parameters
11 changes: 11 additions & 0 deletions site/docs-typr/boundaries/databases/type-safety/date-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ DuckDB has modern date/time handling:
- Microsecond precision
- No timezone conversion issues

### SQLite

SQLite has no native date/time storage class — values are stored as ISO-8601 TEXT (the xerial driver default). Typr generates `LocalDate` / `LocalTime` / `LocalDateTime` / `Instant` codecs that parse and produce the ISO-8601 format, matching the SqliteTypes catalogue.

- `DATE` → `LocalDate` (ISO-8601 date)
- `TIME` → `LocalTime`
- `DATETIME` / `TIMESTAMP` → `LocalDateTime`
- `TIMESTAMP` (with `Z` suffix) → `Instant`

Other SQLite date storage modes (INTEGER unix epoch, REAL Julian day) are configurable via `SqliteConfig.dateClass(...)` but require custom codecs — they don't round-trip through the default Typr codegen.

### Oracle

Oracle uses different type names:
Expand Down
Loading
Loading