From 409744720cf3d8b466a7221deff978d99e3805b4 Mon Sep 17 00:00:00 2001 From: Clancy Date: Wed, 5 Aug 2026 16:11:29 +0300 Subject: [PATCH] docs(complete-suite): add docs content and structure 1- Reorganize all documentation files into category subdirectories (overview, cli, crud, hooks, pagination-and-sort, operations-and-types) 2- Add full documentation guides for Introduction, Client Lifecycle, Schema & ElementType, Enums, Composite Keys, Transactions, Raw SQL, Errors, Init Command, Client Generation, and Migrations 3- Add sidebar-manifest.json as the single source of truth for doc categories, file ordering, and sidebar navigation 4- Update sync-docs.yml GitHub Actions workflow to recursively sync category directories to phi-website --- .github/workflows/sync-docs.yml | 1 + docs/cli/Client-Generation.md | 69 ++++++ docs/cli/Init-Command.md | 115 ++++++++++ docs/cli/Migrations.md | 91 ++++++++ docs/{ => crud}/Count-Records.md | 2 - docs/{ => crud}/Create-Records.md | 2 - docs/{ => crud}/Delete-Records.md | 30 ++- docs/{ => crud}/Omit.md | 2 - docs/{ => crud}/Query-Filters.md | 2 - docs/{ => crud}/Read-Records.md | 2 - docs/{ => crud}/Select.md | 2 - docs/{ => crud}/Update-Records.md | 2 - docs/{ => crud}/Upsert-Records.md | 2 - docs/hooks/Create-Hooks.md | 229 ++++++++++++++++++++ docs/hooks/Delete-Hooks.md | 124 +++++++++++ docs/hooks/Read-Hooks.md | 206 ++++++++++++++++++ docs/hooks/Update-Hooks.md | 143 ++++++++++++ docs/operations-and-types/Composite-Keys.md | 166 ++++++++++++++ docs/operations-and-types/Enums.md | 185 ++++++++++++++++ docs/operations-and-types/Errors.md | 147 +++++++++++++ docs/operations-and-types/Raw-SQL.md | 39 ++++ docs/operations-and-types/Transactions.md | 91 ++++++++ docs/overview/Client-Lifecycle.md | 107 +++++++++ docs/overview/Introduction.md | 39 ++++ docs/overview/Limitations.md | 99 +++++++++ docs/{ => overview}/Quickstart.md | 24 +- docs/overview/Schema.md | 136 ++++++++++++ docs/{ => pagination-and-sort}/Cursor.md | 2 - docs/{ => pagination-and-sort}/OrderBy.md | 2 - docs/{ => pagination-and-sort}/Skip.md | 2 - docs/{ => pagination-and-sort}/Take.md | 2 - docs/sidebar-manifest.json | 38 ++++ docs/template.md | 2 - 33 files changed, 2062 insertions(+), 43 deletions(-) create mode 100644 docs/cli/Client-Generation.md create mode 100644 docs/cli/Init-Command.md create mode 100644 docs/cli/Migrations.md rename docs/{ => crud}/Count-Records.md (97%) rename docs/{ => crud}/Create-Records.md (99%) rename docs/{ => crud}/Delete-Records.md (63%) rename docs/{ => crud}/Omit.md (97%) rename docs/{ => crud}/Query-Filters.md (99%) rename docs/{ => crud}/Read-Records.md (99%) rename docs/{ => crud}/Select.md (99%) rename docs/{ => crud}/Update-Records.md (99%) rename docs/{ => crud}/Upsert-Records.md (99%) create mode 100644 docs/hooks/Create-Hooks.md create mode 100644 docs/hooks/Delete-Hooks.md create mode 100644 docs/hooks/Read-Hooks.md create mode 100644 docs/hooks/Update-Hooks.md create mode 100644 docs/operations-and-types/Composite-Keys.md create mode 100644 docs/operations-and-types/Enums.md create mode 100644 docs/operations-and-types/Errors.md create mode 100644 docs/operations-and-types/Raw-SQL.md create mode 100644 docs/operations-and-types/Transactions.md create mode 100644 docs/overview/Client-Lifecycle.md create mode 100644 docs/overview/Introduction.md create mode 100644 docs/overview/Limitations.md rename docs/{ => overview}/Quickstart.md (83%) create mode 100644 docs/overview/Schema.md rename docs/{ => pagination-and-sort}/Cursor.md (98%) rename docs/{ => pagination-and-sort}/OrderBy.md (98%) rename docs/{ => pagination-and-sort}/Skip.md (98%) rename docs/{ => pagination-and-sort}/Take.md (98%) create mode 100644 docs/sidebar-manifest.json diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index c0eae3e..c5fc392 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -27,6 +27,7 @@ jobs: - name: Copy docs to phi-website content/docs run: | + rm -rf phi-website/content/docs mkdir -p phi-website/content/docs cp -r docs/* phi-website/content/docs/ diff --git a/docs/cli/Client-Generation.md b/docs/cli/Client-Generation.md new file mode 100644 index 0000000..d74acf9 --- /dev/null +++ b/docs/cli/Client-Generation.md @@ -0,0 +1,69 @@ +--- +title: Client Generation +description: Generate strongly-typed Go client code from schema.prisma using the phi generate command. +category: CLI +tags: [cli, generate, client, code-generation, phi-g] +--- + +# Client Generation Command + +The `phi generate` command parses your `schema.prisma` file and generates a 100% type-safe, zero-allocation Go ORM client in the configured output directory. + +--- + +## 1. Quick Usage + +Run the generate command from your project root: + +```bash +phi generate +``` + +Or using the short flag: + +```bash +phi -g +``` + +--- + +## 2. Generation Workflow + +When `phi generate` is executed, the CLI performs the following steps: + +1. **Configuration Resolution**: Reads `phi.yml` or `phi.json` to determine schema input path and client output directory. +2. **Prisma Schema Parsing**: Parses data models, scalar fields, enums, relation constraints, default values, and native `@db.*` types. +3. **Template Rendering**: Executes Phi's internal Go code generation templates: + * **`client.go`**: Core DB client, connection handles, predicate data types, and dialect queries. + * **`[model].go`**: Model structs, query builders, create/update/upsert inputs, select/omit maps, and model delegates. + * **`[model]/[model].go`**: Isolated model sub-packages exporting type-safe field constants (`user.Email`, `user.Role`, etc.). + * **`errors.go` & `validation.go`**: Sentinel error definitions, driver error translation engines, and in-memory input validation guards. +4. **Formatting & Cleanup**: Ensures generated Go source files are cleanly formatted and ready for instant compilation. + +--- + +## 3. When to Re-Run `phi generate` + +Re-run `phi generate` whenever you update your Prisma schema: +* Adding or renaming models or fields. +* Modifying field types or adding `@unique` / `@id` constraints. +* Defining or updating enum values. +* Changing relation rules (`onDelete`). + +> [!TIP] +> Add `phi generate` to your project Makefile or CI build script to ensure generated client code is always in sync with your `schema.prisma`. + +--- + +## 4. Generated Package Architecture + +For a schema containing a `User` model, `phi generate` outputs the following package structure under `./phi`: + +```text +phi/ +├── client.go # Core Client instance, DB handle, raw SQL helpers +├── runtime.go # Dialect execution engine & statement caching +├── user.go # User model struct, UserQueryBuilder, UserCreateBuilder, UserUpdateBuilder +└── user/ + └── user.go # Model package exporting field constants (user.Email, user.Id, etc.) +``` diff --git a/docs/cli/Init-Command.md b/docs/cli/Init-Command.md new file mode 100644 index 0000000..a8d707c --- /dev/null +++ b/docs/cli/Init-Command.md @@ -0,0 +1,115 @@ +--- +title: Init Command +description: Initialize project configuration files (phi.yml / phi.json) using the Phi CLI. +category: CLI +tags: [cli, init, configuration, setup, phi-yml, phi-json] +--- + +# Init Command + +The `phi init` command bootstraps a configuration file for your project. The configuration file dictates schema locations, output directories for generated Go client packages, migration directories, client package naming, embedded migration flags, and logging options. + +--- + +## 1. Quick Usage + +Generate a default YAML configuration file (`phi.yml`) in the current directory: + +```bash +phi init +``` + +Specify a target output directory: + +```bash +phi init ./my-project +``` + +--- + +## 2. Supported Formats + +Phi supports both **YAML** (`.yml` / `.yaml`) and **JSON** (`.json`) configuration formats. + +### YAML (Default) + +```bash +phi init yml [directory] +``` + +Generates `phi.yml`: + +```yaml +# Name of the generated Go client package (default: "phi") +client_name: phi + +# Enable Go 1.16+ embedded migrations (//go:embed) inside client +embed_migrations: true + +database: + url_env: DATABASE_URL # Environment variable for runtime client connections. + direct_url_env: DATABASE_DIRECT_URL # Environment variable used for DDL migrations. + +schema: ./schema.prisma # Path to your Prisma schema. + +output: + client: ./phi # Output directory for generated Go client code. + migrations: ./phi/migrations # Directory for Goose-compatible .sql migration files. + +log: + - none # Logging flags: none, query, warn, error, info, all +``` + +### JSON + +```bash +phi init json [directory] +``` + +Generates `phi.json`: + +```json +{ + "client_name": "phi", + "embed_migrations": true, + "database": { + "url_env": "DATABASE_URL", + "direct_url_env": "DATABASE_DIRECT_URL" + }, + "schema": "./schema.prisma", + "output": { + "client": "./phi", + "migrations": "./phi/migrations" + }, + "log": [ + "none" + ] +} +``` + +--- + +## 3. Configuration Fields Breakdown + +| Field | Description | Default | +| :--- | :--- | :--- | +| `client_name` | Go package name for the generated client code. | `"phi"` | +| `embed_migrations` | Controls whether Go 1.16+ `//go:embed` migration code is generated inside the client. | `true` | +| `database.url_env` | Environment variable holding runtime database connection URL. | `"DATABASE_URL"` | +| `database.direct_url_env` | Environment variable holding migration/direct DDL connection URL. | `"DATABASE_DIRECT_URL"` | +| `schema` | Path to your input `schema.prisma` file. | `"./schema.prisma"` | +| `output.client` | Directory path where generated client code will be saved. | `"./phi"` | +| `output.migrations` | Directory path where versioned `.sql` migration files are stored. | `"./phi/migrations"` | +| `log` | Active logging categories (`none`, `query`, `warn`, `error`, `info`, `all`). | `["none"]` | + +--- + +## 4. Configuration Ambiguity Protection + +To prevent conflicting settings across different files, Phi inspects your project directory for configuration files (`phi.yml`, `phi.yaml`, `phi.json`). + +If **more than one** configuration file is detected in the same directory (e.g. both `phi.yml` and `phi.json`), Phi CLI immediately halts with a fatal error: + +```text +FATAL: multiple configuration files found (phi.yml, phi.json). Please keep only one configuration file in the project directory. +``` diff --git a/docs/cli/Migrations.md b/docs/cli/Migrations.md new file mode 100644 index 0000000..41411b0 --- /dev/null +++ b/docs/cli/Migrations.md @@ -0,0 +1,91 @@ +--- +title: Migrations +description: Declarative, forward-only migration workflows and Goose-compatible DDL file generation in Phi. +category: CLI +tags: [migrations, goose, atlas, ddl, schema-diff, phi-migrate] +--- + +# Migrations + +Phi features a production-grade, **forward-only** migration engine (`phi migrate`). Inspired by Prisma's declarative migration workflow and powered by the Atlas DDL calculation engine, Phi diffs your `schema.prisma` against your database state and generates standard, Goose-compatible SQL migration files. + +--- + +## 1. Migration Model: Forward-Only + +Phi adopts a **forward-only** migration strategy. In production database engineering, rolling back migrations via down scripts frequently leads to data corruption or accidental column dropping. + +When your schema evolves: + +1. Update `schema.prisma`. +2. Run `phi migrate `. +3. Phi calculates the exact SQL delta, appends a new versioned `.sql` migration file, and executes it. + +--- + +## 2. CLI Migration Workflows + +### Generating & Applying Migrations + +Run the CLI command to create and apply a migration: + +```bash +phi migrate +``` + +Or using the short flag: + +```bash +phi -m +``` + +### What Happens During `phi migrate`: + +1. **Database Initialization**: Creates the target database automatically if it does not already exist. +2. **Schema Diffing**: Uses Atlas DDL engine to compare your Prisma schema against current database tables. +3. **Goose SQL Generation**: Writes a versioned SQL file (e.g. `./phi/migrations/00001_init.sql`) containing standard Goose migration headers: + ```sql + -- +goose Up + CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + PRIMARY KEY ("id") + ); + ``` +4. **Execution**: Applies the migration immediately to the database. + +--- + +## 3. Goose & Embed Compatibility + +Because Phi migration files use standard Goose `-- +goose Up` headers, you can embed migration scripts directly into your Go binaries using Go 1.16+ `embed.FS`: + +```go +package main + +import ( + "embed" + "github.com/pressly/goose/v3" +) + +//go:embed phi/migrations/*.sql +var embedMigrations embed.FS + +func runEmbeddedMigrations(db *sql.DB) error { + goose.SetBaseFS(embedMigrations) + return goose.Up(db, "phi/migrations") +} +``` + +--- + +## 4. Dialect-Aware Migration DDL + +Phi's migration generator adjusts DDL syntax based on your `schema.prisma` `datasource db { provider = "..." }`: + +| Feature | PostgreSQL DDL | SQLite DDL | +| :-------------------- | :---------------------------------------- | :--------------------------------------- | +| **Enums** | Native `CREATE TYPE "Enum" AS ENUM (...)` | Column `TEXT` + `CHECK ("col" IN (...))` | +| **UUID Primary Keys** | `UUID NOT NULL` | `TEXT NOT NULL` | +| **AutoIncrement** | `BIGSERIAL` / `SERIAL` | `INTEGER PRIMARY KEY AUTOINCREMENT` | +| **JSON / JSONB** | `JSONB` | `TEXT` / `BLOB` | diff --git a/docs/Count-Records.md b/docs/crud/Count-Records.md similarity index 97% rename from docs/Count-Records.md rename to docs/crud/Count-Records.md index e1c3fe0..dce3941 100644 --- a/docs/Count-Records.md +++ b/docs/crud/Count-Records.md @@ -3,8 +3,6 @@ title: Count Records description: Count the number of records matching a set of predicates. category: CRUD tags: [count, count records] -categoryOrder: 2 -order: 6 --- # Count Records diff --git a/docs/Create-Records.md b/docs/crud/Create-Records.md similarity index 99% rename from docs/Create-Records.md rename to docs/crud/Create-Records.md index ef5619b..5db5cfd 100644 --- a/docs/Create-Records.md +++ b/docs/crud/Create-Records.md @@ -3,8 +3,6 @@ title: Create Records description: Create one or more records. category: CRUD tags: [create, createMany, createManyAndReturn, onConflict, conflictAction] -categoryOrder: 2 -order: 1 --- # Create Records diff --git a/docs/Delete-Records.md b/docs/crud/Delete-Records.md similarity index 63% rename from docs/Delete-Records.md rename to docs/crud/Delete-Records.md index 4ec7e0c..fcedd9e 100644 --- a/docs/Delete-Records.md +++ b/docs/crud/Delete-Records.md @@ -2,9 +2,7 @@ title: Delete Records description: Delete one or more records using type-safe delete builders. category: CRUD -tags: [delete, deleteMany] -categoryOrder: 2 -order: 4 +tags: [delete, deleteMany, relations, cascade, foreign-key] --- # Delete Records @@ -18,6 +16,30 @@ Phi provides two methods for deleting records: --- +## Relation Deletion Behavior (`onDelete`) + +Deletion behavior for foreign key relations is controlled by your `schema.prisma` `@relation(onDelete: ...)` rules: + +```prisma +model Post { + id String @id @default(cuid()) + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) +} +``` + +Phi translates `@relation(onDelete: ...)` rules into database foreign key constraints in DDL: + +| Prisma `onDelete` Action | SQL Foreign Key DDL Action | Database Behavior on Deleting Parent Record | +| :----------------------- | :------------------------- | :------------------------------------------------------------------------------------- | +| `Cascade` | `ON DELETE CASCADE` | Child records referencing the parent are automatically deleted by the database engine. | +| `Restrict` | `ON DELETE RESTRICT` | Prevents deletion of the parent record if dependent child records exist. | +| `NoAction` | `ON DELETE NO ACTION` | Prevents deletion unless constraints are satisfied within the transaction. | +| `SetNull` | `ON DELETE SET NULL` | Sets foreign key columns on referencing child records to `NULL`. | +| `SetDefault` | `ON DELETE SET DEFAULT` | Resets foreign key columns on child records to their schema default values. | + +--- + ## Delete Deletes a **single record**. @@ -71,7 +93,7 @@ user, err := db.User.Delete( ### Supported Builder Methods | Method | Description | -| ------------------------ | ------------------------------------------------- | +| :----------------------- | :------------------------------------------------ | | [`Select`](/docs/Select) | Return only the selected fields and relations. | | [`Omit`](/docs/Omit) | Return all scalar fields except the omitted ones. | diff --git a/docs/Omit.md b/docs/crud/Omit.md similarity index 97% rename from docs/Omit.md rename to docs/crud/Omit.md index cbc441b..afde209 100644 --- a/docs/Omit.md +++ b/docs/crud/Omit.md @@ -3,8 +3,6 @@ title: Omit description: Omit specific scalar fields from a query result. category: Select & Omit tags: [omit, omit fields, scalar selection] -categoryOrder: 3 -order: 2 --- # Omit diff --git a/docs/Query-Filters.md b/docs/crud/Query-Filters.md similarity index 99% rename from docs/Query-Filters.md rename to docs/crud/Query-Filters.md index af77a62..86d8a26 100644 --- a/docs/Query-Filters.md +++ b/docs/crud/Query-Filters.md @@ -3,8 +3,6 @@ title: Query Filters description: The predicate system for filtering records with EQ, NEQ, GT, LT, In, Contains, Like, and logical operators. category: Query Filters tags: [query, filters, eq, neq, gt, lt, in, contains, like, and, or, not, predicates] -categoryOrder: 4 -order: 1 --- # Query Filters diff --git a/docs/Read-Records.md b/docs/crud/Read-Records.md similarity index 99% rename from docs/Read-Records.md rename to docs/crud/Read-Records.md index b61bde7..0763841 100644 --- a/docs/Read-Records.md +++ b/docs/crud/Read-Records.md @@ -3,8 +3,6 @@ title: Read Records description: Query records using FindUnique, FindFirst, and FindMany. category: CRUD tags: [read, query, findUnique, findFirst, findMany] -categoryOrder: 2 -order: 2 --- # Read Records diff --git a/docs/Select.md b/docs/crud/Select.md similarity index 99% rename from docs/Select.md rename to docs/crud/Select.md index 9d530e3..86420a1 100644 --- a/docs/Select.md +++ b/docs/crud/Select.md @@ -13,8 +13,6 @@ tags: omit fields, omit relations, ] -categoryOrder: 3 -order: 1 --- # Select diff --git a/docs/Update-Records.md b/docs/crud/Update-Records.md similarity index 99% rename from docs/Update-Records.md rename to docs/crud/Update-Records.md index 628a35c..94f1123 100644 --- a/docs/Update-Records.md +++ b/docs/crud/Update-Records.md @@ -3,8 +3,6 @@ title: Update Records description: Update one or more records using type-safe update builders. category: CRUD tags: [update, updateMany, updateManyAndReturn] -categoryOrder: 2 -order: 3 --- # Update Records diff --git a/docs/Upsert-Records.md b/docs/crud/Upsert-Records.md similarity index 99% rename from docs/Upsert-Records.md rename to docs/crud/Upsert-Records.md index e416fc6..0d07e36 100644 --- a/docs/Upsert-Records.md +++ b/docs/crud/Upsert-Records.md @@ -3,8 +3,6 @@ title: Upsert Records description: Configure conflict handling for create operations. category: CRUD tags: [upsert, onConflict, conflictAction, create, createManyAndReturn] -categoryOrder: 2 -order: 5 --- # Upsert Records diff --git a/docs/hooks/Create-Hooks.md b/docs/hooks/Create-Hooks.md new file mode 100644 index 0000000..254ab57 --- /dev/null +++ b/docs/hooks/Create-Hooks.md @@ -0,0 +1,229 @@ +--- +title: Create Hooks +description: Intercept and mutate record creation with extension hooks. +category: Hooks +tags: [hooks, extension, create, createMany, createManyAndReturn, middleware] +--- + +# Create Hooks + +Create hooks allow you to intercept `Create`, `CreateMany`, and `CreateManyAndReturn` operations before queries hit the database. You can validate inputs, mutate fields, handle upsert conflicts, or short-circuit query execution entirely. + +--- + +## Schema Context + +The examples in this guide reference the following Prisma schema: + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + password String? + role UserRole @default(STUDENT) + loginCount Int @default(0) + createdAt DateTime @default(now()) + + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + authorId String? +} +``` + +--- + +## Registration & Chaining + +Register hooks on a model delegate using `.Use()`, passing a model extension (e.g. `user.Extension`): + +```go +db.User.Use(user.Extension{ + Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) { + // Pre-hook: runs before database execution + args.Data.Email = strings.ToLower(args.Data.Email) + + res, err := next(ctx, args) + + // Post-hook: runs after database execution + return res, err + }, +}) +``` + +### Chaining Order & Context Flow + +Multiple `.Use()` calls stack in middleware order (outermost to innermost): + +``` +Request → Hook A (Pre) → Hook B (Pre) → Database → Hook B (Post) → Hook A (Post) → Response +``` + +> **Note:** Context values set via `context.WithValue` flow inward to subsequent hooks and query execution, but are isolated from outer hooks during unwinding. + +--- + +## Hook Signatures + +| Hook Field | Signature | Return Type | +| :-------------------- | :-------------------------------------------------------------------------------------------------------- | :--------------------- | +| `Create` | `func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery)` | `(*phi.User, error)` | +| `CreateMany` | `func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery)` | `(int64, error)` | +| `CreateManyAndReturn` | `func(ctx context.Context, args *phi.UserCreateManyAndReturnArgs, next phi.UserCreateManyAndReturnQuery)` | `([]*phi.User, error)` | + +--- + +## 1. Single Record (`Create`) + +`*phi.UserCreateArgs` exposes the following query properties: + +| Field | Type | Description | +| :--------------- | :--------------------------- | :------------------------------------------------------------- | +| `Data` | `*phi.UserCreate` | The record fields being inserted. | +| `Select` | `*phi.UserSelect` | Selected scalar and relation fields to return. | +| `ConflictTarget` | `phi.UniqueConstraintTarget` | Unique column or composite key for upserts. | +| `ConflictAction` | `*phi.ConflictAction` | Resolution action (`DoNothing`, `UpdateNewValues`, or custom). | + +### Mutating Inputs + +Modify `args.Data` directly before invoking `next`: + +```go +db.User.Use(user.Extension{ + Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) { + // Normalize email + args.Data.Email = strings.ToLower(strings.TrimSpace(args.Data.Email)) + + // Hash password if present + if args.Data.Password != nil { + hashed := hashPassword(*args.Data.Password) + args.Data.Password = &hashed + } + + return next(ctx, args) + }, +}) +``` + +> **Note:** Required fields are concrete types, as they must be provided, no need for `nil` check, optional fields are always pointers, as they may be `nil` to indicate "unset". + +### Short-Circuiting Execution + +Return early without calling `next(ctx, args)` to bypass database insertion: + +```go +db.User.Use(user.Extension{ + Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) { + if args.Data.Email == "" { + return nil, errors.New("email is required") + } + return next(ctx, args) + }, +}) +``` + +### Relation Sub-queries & Selections + +Customize returned scalar fields and pre-load relations on `args.Select`: + +```go +db.User.Use(user.Extension{ + Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) { + args.Select.Email = true + args.Select.Posts = post.Query().Where(post.Published.EQ(true)) + return next(ctx, args) + }, +}) +``` + +--- + +## 2. Bulk Insert (`CreateMany`) + +`*phi.UserCreateManyArgs` exposes: + +- `Data []*phi.UserCreate`: Slice of record inputs. +- `ConflictTarget` & `ConflictAction`: Upsert rules for bulk conflicts. + +### Batch Input Mutation & Validation + +Iterate over `args.Data` to validate or modify entries in-place: + +```go +db.User.Use(user.Extension{ + CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) { + for _, record := range args.Data { + record.Email = strings.ToLower(record.Email) + } + return next(ctx, args) + }, +}) +``` + +### Appending Records + +Use `args.AppendData(...)` to dynamically inject extra records into the batch: + +```go +db.User.Use(user.Extension{ + CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) { + args.AppendData( + db.User.Create().SetEmail("audit1@example.com").SetPhoneNum("+1001"), + db.User.Create().SetEmail("audit2@example.com").SetPhoneNum("+1002"), + ) + return next(ctx, args) + }, +}) +``` + +--- + +## 3. Bulk Insert and Return (`CreateManyAndReturn`) + +`*phi.UserCreateManyAndReturnArgs` combines batch data manipulation with relation selection: + +```go +db.User.Use(user.Extension{ + CreateManyAndReturn: func(ctx context.Context, args *phi.UserCreateManyAndReturnArgs, next phi.UserCreateManyAndReturnQuery) ([]*phi.User, error) { + // Enforce default role across all batch entries + defaultRole := phi.UserRole_STUDENT + for _, record := range args.Data { + if record.Role == nil { + record.Role = &defaultRole + } + } + + // Return nested posts for all created users + args.Select.Posts = post.Query() + + return next(ctx, args) + }, +}) +``` + +--- + +## 4. Upsert & Conflict Interception + +Intercept conflict resolution strategies (`OnConflict`) across single and bulk creation: + +```go +db.User.Use(user.Extension{ + CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) { + if args.ConflictAction != nil && args.ConflictAction.IsUpdateNewValues() { + // Override conflict action with a custom update builder + args.ConflictAction = user.ConflictUpdate(func(u *phi.UserUpsert) { + u.Role.Set(phi.UserRole_STUDENT) + u.LoginCount.Increment(1) + }) + } + return next(ctx, args) + }, +}) +``` diff --git a/docs/hooks/Delete-Hooks.md b/docs/hooks/Delete-Hooks.md new file mode 100644 index 0000000..0e24d71 --- /dev/null +++ b/docs/hooks/Delete-Hooks.md @@ -0,0 +1,124 @@ +--- +title: Delete Hooks +description: Intercept and mutate record deletion with extension hooks. +category: Hooks +tags: [hooks, extension, delete, deleteMany, predicate, middleware] +--- + +# Delete Hooks + +Delete hooks allow you to intercept `Delete` and `DeleteMany` operations before queries hit the database. You can enforce safety rules (preventing root user or mass table deletion), restrict deletion targets, or return preloaded relations on deleted records. + +--- + +## Schema Context + +The examples in this guide reference the following Prisma schema: + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + password String? + role UserRole @default(STUDENT) + loginCount Int @default(0) + createdAt DateTime @default(now()) + + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + authorId String? +} +``` + +--- + +## Registration & Chaining + +Register hooks on a model delegate using `.Use()`, passing a model extension (e.g. `user.Extension`): + +```go +db.User.Use(user.Extension{ + Delete: func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery) (*phi.User, error) { + // Pre-hook: prevent deletion of protected user + for _, p := range args.Where { + if p.Column() == user.Email.Column && p.Value() == "admin@example.com" { + return nil, errors.New("cannot delete primary admin user") + } + } + + res, err := next(ctx, args) + + // Post-hook: runs after successful deletion + return res, err + }, +}) +``` + +--- + +## Hook Signatures + +| Hook Field | Signature | Return Type | +| :--- | :--- | :--- | +| `Delete` | `func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery)` | `(*phi.User, error)` | +| `DeleteMany` | `func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery)` | `(int64, error)` | + +--- + +## 1. Safety Guards & Aborting Deletion + +To abort a deletion operation, return an error before invoking `next(ctx, args)`. The database is never queried: + +### Guarding Against Mass Deletion (`DeleteMany`) +```go +db.User.Use(user.Extension{ + DeleteMany: func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery) (int64, error) { + // Refuse empty WHERE clause (prevent truncating entire table) + if len(args.Where) == 0 { + return 0, errors.New("unbounded bulk delete is rejected for safety") + } + return next(ctx, args) + }, +}) +``` + +--- + +## 2. Restricting Target Predicates (`Where`) + +Inspect `args.Where` or append filters to enforce scoping (e.g., tenant boundaries): + +```go +db.User.Use(user.Extension{ + DeleteMany: func(ctx context.Context, args *phi.UserDeleteManyArgs, next phi.UserDeleteManyQuery) (int64, error) { + // Only allow deleting STUDENT accounts + args.Where = append(args.Where, user.Role.EQ(phi.UserRole_STUDENT)) + return next(ctx, args) + }, +}) +``` + +--- + +## 3. Preloading Relations on Deleted Records (`Delete`) + +`Delete` carries `args.Select`, allowing you to retrieve selected scalar fields and preloaded relations of the record being deleted: + +```go +db.User.Use(user.Extension{ + Delete: func(ctx context.Context, args *phi.UserDeleteArgs, next phi.UserDeleteQuery) (*phi.User, error) { + // Return deleted user along with their posts + args.Select.Email = true + args.Select.Posts = post.Query() + + return next(ctx, args) + }, +}) +``` diff --git a/docs/hooks/Read-Hooks.md b/docs/hooks/Read-Hooks.md new file mode 100644 index 0000000..aa79f30 --- /dev/null +++ b/docs/hooks/Read-Hooks.md @@ -0,0 +1,206 @@ +--- +title: Read Hooks +description: Intercept and mutate read queries with extension hooks. +category: Hooks +tags: [hooks, extension, findUnique, findFirst, findMany, count, predicate, middleware] +--- + +# Read Hooks + +Read hooks allow you to intercept `FindUnique`, `FindFirst`, `FindMany`, and `Count` operations before queries hit the database. You can inspect or append predicates, apply soft-delete/tenant filters, modify ordering or pagination, or short-circuit queries with cached data. + +--- + +## Schema Context + +The examples in this guide reference the following Prisma schema: + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + password String? + role UserRole @default(STUDENT) + loginCount Int @default(0) + createdAt DateTime @default(now()) + + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + authorId String? +} +``` + +--- + +## Registration & Chaining + +Register hooks on a model delegate using `.Use()`, passing a model extension (e.g. `user.Extension`): + +```go +db.User.Use(user.Extension{ + FindMany: func(ctx context.Context, args *phi.UserFindManyArgs, next phi.UserFindManyQuery) ([]*phi.User, error) { + // Pre-hook: append a global tenant filter + args.Where = append(args.Where, user.LoginCount.GTE(1)) + + res, err := next(ctx, args) + + // Post-hook: process or cache results + return res, err + }, +}) +``` + +--- + +## Hook Signatures + +| Hook Field | Signature | Return Type | +| :--- | :--- | :--- | +| `FindUnique` | `func(ctx context.Context, args *phi.UserFindUniqueArgs, next phi.UserFindUniqueQuery)` | `(*phi.User, error)` | +| `FindFirst` | `func(ctx context.Context, args *phi.UserFindFirstArgs, next phi.UserFindFirstQuery)` | `(*phi.User, error)` | +| `FindMany` | `func(ctx context.Context, args *phi.UserFindManyArgs, next phi.UserFindManyQuery)` | `([]*phi.User, error)` | +| `Count` | `func(ctx context.Context, args *phi.UserCountArgs, next phi.UserCountQuery)` | `(int64, error)` | + +--- + +## 1. Inspecting & Appending Predicates (`Where`) + +All read args contain `Where []phi.PredicateOf[User]`. Each predicate exposes `.Column()`, `.Value()`, and `.Children()` for type-safe inspection: + +```go +db.User.Use(user.Extension{ + FindMany: func(ctx context.Context, args *phi.UserFindManyArgs, next phi.UserFindManyQuery) ([]*phi.User, error) { + // Inspect active filters + for _, pred := range args.Where { + if children := pred.Children(); len(children) > 0 { + // Composite key predicate (e.g. @@unique([email, phoneNum])) + for _, child := range children { + fmt.Printf("Composite constituent: %s = %v\n", child.Column, child.Value) + } + } else { + // Standard scalar predicate + fmt.Printf("Filter applied: %s = %v\n", pred.Column(), pred.Value()) + } + } + + // Enforce role restriction + args.Where = append(args.Where, user.Role.EQ(phi.UserRole_STUDENT)) + + return next(ctx, args) + }, +}) +``` + +### Combining Predicates with Logical Operators (`Or` / `And`) +Group predicates using logical operators: + +```go +args.Where = append(args.Where, + user.Or( + user.Role.EQ(phi.UserRole_ADMIN), + user.Role.EQ(phi.UserRole_TEACHER), + ), +) +``` + +--- + +## 2. Setters vs. Direct Mutation + +Every query argument struct provides chainable `Set*` helper methods for replacing values, as well as direct exported fields for appending: + +| Setter Method | Target Arguments | Replaces Field | +| :--- | :--- | :--- | +| `SetWhere(...)` | `FindUnique`, `FindFirst`, `FindMany`, `Count` | `Where` | +| `SetOrderBy(...)` | `FindFirst`, `FindMany` | `OrderBy` | +| `SetCursor(...)` | `FindFirst`, `FindMany` | `Cursor` | +| `SetSkip(n int)` | `FindFirst`, `FindMany`, `Count` | `Skip` (`*int`) | +| `SetTake(n int)` | `FindFirst`, `FindMany`, `Count` | `Take` (`*int`) | + +### Example: Chainable Setters (`FindMany`) +```go +db.User.Use(user.Extension{ + FindMany: func(ctx context.Context, args *phi.UserFindManyArgs, next phi.UserFindManyQuery) ([]*phi.User, error) { + args.SetWhere(user.LoginCount.GTE(10)). + SetOrderBy(user.LoginCount.Desc(), user.Email.Asc()). + SetSkip(0). + SetTake(50) + + return next(ctx, args) + }, +}) +``` + +--- + +## 3. Operations Overview + +### `FindUnique` +Receives `*phi.UserFindUniqueArgs` with `Where []phi.PredicateOf[User]` and `Select *phi.UserSelect`. + +```go +db.User.Use(user.Extension{ + FindUnique: func(ctx context.Context, args *phi.UserFindUniqueArgs, next phi.UserFindUniqueQuery) (*phi.User, error) { + // Force select user posts + args.Select.Posts = post.Query().Where(post.Published.EQ(true)) + + return next(ctx, args) + }, +}) +``` + +### `FindFirst` & `FindMany` +Support full pagination, sorting, and relation preloading: + +```go +db.User.Use(user.Extension{ + FindFirst: func(ctx context.Context, args *phi.UserFindFirstArgs, next phi.UserFindFirstQuery) (*phi.User, error) { + // Enforce order by highest login count + args.SetOrderBy(user.LoginCount.Desc()) + return next(ctx, args) + }, +}) +``` + +### `Count` +Receives `*phi.UserCountArgs` with `Where`, `Skip`, and `Take`: + +```go +db.User.Use(user.Extension{ + Count: func(ctx context.Context, args *phi.UserCountArgs, next phi.UserCountQuery) (int64, error) { + args.Where = append(args.Where, user.LoginCount.GT(0)) + return next(ctx, args) + }, +}) +``` + +--- + +## 4. Query Short-Circuiting & Caching + +Skip database calls by returning cached data directly: + +```go +db.User.Use(user.Extension{ + FindUnique: func(ctx context.Context, args *phi.UserFindUniqueArgs, next phi.UserFindUniqueQuery) (*phi.User, error) { + cacheKey := fmt.Sprintf("user:%v", args.Where[0].Value()) + + if cached, found := cache.Get(cacheKey); found { + return cached.(*phi.User), nil + } + + res, err := next(ctx, args) + if err == nil && res != nil { + cache.Set(cacheKey, res) + } + return res, err + }, +}) +``` diff --git a/docs/hooks/Update-Hooks.md b/docs/hooks/Update-Hooks.md new file mode 100644 index 0000000..b72df27 --- /dev/null +++ b/docs/hooks/Update-Hooks.md @@ -0,0 +1,143 @@ +--- +title: Update Hooks +description: Intercept and mutate record updates with extension hooks. +category: Hooks +tags: [hooks, extension, update, updateMany, updateManyAndReturn, predicate, middleware] +--- + +# Update Hooks + +Update hooks allow you to intercept `Update`, `UpdateMany`, and `UpdateManyAndReturn` operations before queries hit the database. You can validate or hash input fields, auto-stamp audit timestamps, mutate target predicates, or pre-load relations on returned records. + +--- + +## Schema Context + +The examples in this guide reference the following Prisma schema: + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + password String? + role UserRole @default(STUDENT) + loginCount Int @default(0) + createdAt DateTime @default(now()) + + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + authorId String? +} +``` + +--- + +## Registration & Chaining + +Register hooks on a model delegate using `.Use()`, passing a model extension (e.g. `user.Extension`): + +```go +db.User.Use(user.Extension{ + Update: func(ctx context.Context, args *phi.UserUpdateArgs, next phi.UserUpdateQuery) (*phi.User, error) { + // Pre-hook: normalize email if being updated + if args.Data.Email != nil { + lower := strings.ToLower(*args.Data.Email) + args.Data.Email = &lower + } + + res, err := next(ctx, args) + + // Post-hook: runs after query execution + return res, err + }, +}) +``` + +--- + +## Hook Signatures + +| Hook Field | Signature | Return Type | +| :--- | :--- | :--- | +| `Update` | `func(ctx context.Context, args *phi.UserUpdateArgs, next phi.UserUpdateQuery)` | `(*phi.User, error)` | +| `UpdateMany` | `func(ctx context.Context, args *phi.UserUpdateManyArgs, next phi.UserUpdateManyQuery)` | `(int64, error)` | +| `UpdateManyAndReturn` | `func(ctx context.Context, args *phi.UserUpdateManyAndReturnArgs, next phi.UserUpdateManyAndReturnQuery)` | `([]*phi.User, error)` | + +--- + +## 1. Mutating Update Data (`args.Data`) + +All update structs use **optional pointer fields** (`*string`, `*int32`, etc.). +* `nil`: The field is **unmodified** by the query. +* `non-nil`: The field **will be updated** to the dereferenced value. + +```go +db.User.Use(user.Extension{ + Update: func(ctx context.Context, args *phi.UserUpdateArgs, next phi.UserUpdateQuery) (*phi.User, error) { + // Lowercase email if updated + if args.Data.Email != nil { + lower := strings.ToLower(*args.Data.Email) + args.Data.Email = &lower + } + + // Hash password if updated + if args.Data.Password != nil { + hashed := hashPassword(*args.Data.Password) + args.Data.Password = &hashed + } + + return next(ctx, args) + }, +}) +``` + +--- + +## 2. Targeting & Where Clauses (`Where`) + +Inspect or append predicates to restrict which records can be updated: + +```go +db.User.Use(user.Extension{ + UpdateMany: func(ctx context.Context, args *phi.UserUpdateManyArgs, next phi.UserUpdateManyQuery) (int64, error) { + // Restrict bulk updates to STUDENT accounts only + args.Where = append(args.Where, user.Role.EQ(phi.UserRole_STUDENT)) + return next(ctx, args) + }, +}) +``` + +### Setters for Retargeting +`SetWhere` replaces the active target filter: + +```go +db.User.Use(user.Extension{ + Update: func(ctx context.Context, args *phi.UserUpdateArgs, next phi.UserUpdateQuery) (*phi.User, error) { + args.SetWhere(user.Email.EQ("target@example.com")) + return next(ctx, args) + }, +}) +``` + +--- + +## 3. Relation Selection on Returned Records + +`Update` and `UpdateManyAndReturn` support `args.Select` for preloading relations on updated records: + +```go +db.User.Use(user.Extension{ + UpdateManyAndReturn: func(ctx context.Context, args *phi.UserUpdateManyAndReturnArgs, next phi.UserUpdateManyAndReturnQuery) ([]*phi.User, error) { + // Return updated users along with their published posts + args.Select.Posts = post.Query().Where(post.Published.EQ(true)) + return next(ctx, args) + }, +}) +``` diff --git a/docs/operations-and-types/Composite-Keys.md b/docs/operations-and-types/Composite-Keys.md new file mode 100644 index 0000000..c2b9f54 --- /dev/null +++ b/docs/operations-and-types/Composite-Keys.md @@ -0,0 +1,166 @@ +--- +title: Composite Keys +description: Working with multi-column primary keys (@@id) and compound unique constraints (@@unique). +category: Schema & Types +tags: [composite, primary-key, unique, predicates, multi-column] +--- + +# Composite Keys + +Phi provides full support for multi-column primary keys (`@@id`) and compound unique constraints (`@@unique`). Composite keys are exposed as first-class generated objects with multi-argument `.EQ(...)` predicates, full CRUD support, `.Children()` inspection methods, and seamless integration with extension hooks and upserts. + +--- + +## Schema Definitions + +### 1. Composite Unique Constraint (`@@unique`) +```prisma +model User { + id String @id @default(cuid()) + email String + phoneNum String + + @@unique([email, phoneNum]) +} +``` +* **Generated Target**: `user.EmailPhone` + +### 2. Composite Primary Key (`@@id`) +```prisma +model CategoryToPost { + postId String + categoryId Int + + @@id([postId, categoryId]) +} +``` +* **Generated Target**: `categoryToPost.PostId_CategoryId` + +--- + +## Access Pattern & Predicates (`EQ`) + +For composite keys, Phi generates a composite helper struct in the model package (e.g. `user.EmailPhone` or `categoryToPost.PostId_CategoryId`). + +### Multi-Argument `EQ(...)` Predicate +Call `.EQ(...)` passing values in the exact order specified in the Prisma schema: + +```go +// Matches (email = 'a@b.com' AND phoneNum = '+1000') +p1 := user.EmailPhone.EQ("a@b.com", "+1000") + +// Matches (postId = 'post-1' AND categoryId = 42) +p2 := categoryToPost.PostId_CategoryId.EQ("post-1", 42) +``` + +### Predicate Inspection & `.Children()` Method +When inspecting a composite predicate: +* **`.Column()`**: Returns the logical composite column name (e.g. `"emailPhone"` or `"PostId_CategoryId"`). +* **`.Children()`**: Returns a slice of `phi.ChildPredicate` containing constituent child columns and values (`[]ChildPredicate{{Column: "email", Value: "a@b.com"}, ...}}`). +* **`.Value()`**: Returns a `map[string]any` holding constituent column keys and values. + +```go +p := user.EmailPhone.EQ("a@b.com", "+1000") + +fmt.Println(p.Column()) // "emailPhone" + +// Inspect constituent columns using Children() +for _, child := range p.Children() { + fmt.Printf("%s = %v\n", child.Column, child.Value) + // email = a@b.com + // phoneNum = +1000 +} +``` + +--- + +## CRUD Operations with Composite Keys + +### 1. `FindUnique` by Composite Key + +Fetch a single record targeting a compound unique or composite primary key: + +```go +// Unique lookup via composite @@unique +u, err := db.User.FindUnique( + user.EmailPhone.EQ("user@example.com", "+1000"), +).Exec(ctx) + +// Unique lookup via composite @@id +ctp, err := db.CategoryToPost.FindUnique( + categoryToPost.PostId_CategoryId.EQ("post-10", 42), +).Exec(ctx) +``` + +### 2. Combining Additional Predicates +You can pass additional scalar predicates alongside a composite key in `FindUnique`: + +```go +u, err := db.User.FindUnique( + user.EmailPhone.EQ("user@example.com", "+1000"), + user.Role.EQ(phi.UserRole_STUDENT), +).Exec(ctx) +``` + +### 3. Updating Records by Composite Key + +Update records matching a composite key: + +```go +u, err := db.User.Update( + user.EmailPhone.EQ("user@example.com", "+1000"), +).SetPassword("new-secret").Exec(ctx) +``` + +### 4. Deleting Records by Composite Key + +Delete join table or composite records: + +```go +deleted, err := db.CategoryToPost.Delete( + categoryToPost.PostId_CategoryId.EQ("post-10", 42), +).Exec(ctx) +``` + +--- + +## Conflict Resolution (`OnConflict`) with Composite Keys + +Use composite keys directly as `OnConflict` targets for upsert queries: + +```go +// Ignore duplicate composite inserts +affected, err := db.CategoryToPost.CreateMany( + db.CategoryToPost.Create().SetPostId("post-1").SetCategoryId(42), +).OnConflict(categoryToPost.PostId_CategoryId).Ignore().Exec(ctx) + +// Update existing matching composite record +affected, err := db.User.CreateMany( + db.User.Create().SetEmail("user@example.com").SetPhoneNum("+1000").SetPassword("updated"), +).OnConflict(user.EmailPhone).UpdateNewValues().Exec(ctx) +``` + +--- + +## Extension Hooks & Composite Keys + +In extension hooks, a composite predicate appears as a single logical column in `args.Where`. Use `.Children()` to iterate over its constituent fields without type casting: + +```go +db.User.Use(user.Extension{ + FindUnique: func(ctx context.Context, args *phi.UserFindUniqueArgs, next phi.UserFindUniqueQuery) (*phi.User, error) { + for _, w := range args.Where { + switch w.Column() { + case user.Email.Column: + // Scalar unique predicate + case user.EmailPhone.Column: + // Iterate constituent fields with .Children() + for _, child := range w.Children() { + fmt.Printf("Composite field: %s = %v\n", child.Column, child.Value) + } + } + } + return next(ctx, args) + }, +}) +``` diff --git a/docs/operations-and-types/Enums.md b/docs/operations-and-types/Enums.md new file mode 100644 index 0000000..375b526 --- /dev/null +++ b/docs/operations-and-types/Enums.md @@ -0,0 +1,185 @@ +--- +title: Enums +description: Define, query, migrate, and enforce type-safe enum values across PostgreSQL and SQLite. +category: Schema & Types +tags: [enum, postgresql, sqlite, migrations, ddl, type-safety, validation] +--- + +# Enums + +Phi generates strongly-typed Go constants, validation routines, and dialect-specific database DDL for Prisma `enum` definitions. Enums provide compile-time type safety across your client builders, input structs, and query predicates while mapping cleanly to PostgreSQL and SQLite migrations. + +--- + +## Defining Enums in Prisma Schema + +Define an enum in your `schema.prisma`: + +```prisma +enum UserRole { + ADMIN + STUDENT + TEACHER +} + +model User { + id String @id @default(cuid()) + email String @unique + role UserRole @default(STUDENT) +} +``` + +--- + +## Dialect Storage & DDL Generation + +Phi's migration engine (`phi migrate`) tailors DDL generation based on the database provider: + +### 1. PostgreSQL (Native DDL Enums) + +PostgreSQL natively supports custom enum types. During migration generation, Phi generates a dedicated `CREATE TYPE` statement followed by an enum-typed column: + +#### Generated DDL: +```sql +CREATE TYPE "UserRole" AS ENUM ( + 'ADMIN', + 'STUDENT', + 'TEACHER' +); + +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'STUDENT', + PRIMARY KEY ("id") +); +``` + +* **Storage**: Stored using PostgreSQL's internal 4-byte enum OID storage engine. +* **Enforcement**: Database engine strictly rejects non-enum string values at the SQL driver boundary. + +--- + +### 2. SQLite (TEXT Storage + Inline CHECK Constraints) + +SQLite does not have native enum types. Phi creates the column as `TEXT` and appends an inline SQL `CHECK` constraint: + +#### Generated DDL: +```sql +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" TEXT NOT NULL DEFAULT 'STUDENT', + PRIMARY KEY ("id"), + CONSTRAINT "user_role_check" CHECK ("role" IN ('ADMIN', 'STUDENT', 'TEACHER')) +); +``` + +* **Storage**: Stored as plain UTF-8 `TEXT`. +* **Enforcement**: + 1. **Database DDL**: SQL `CHECK` constraint prevents invalid strings from being inserted directly via raw SQL. + 2. **Application Level**: Phi generates `UserRoleType` Go types with `.IsValid()` checks, guaranteeing strict type safety before queries hit SQLite. + +--- + +## Default Value Formatting + +When an `@default(...)` decorator is added to an enum field in your Prisma schema: + +```prisma +role UserRole @default(STUDENT) +``` + +Phi's migration engine formats default values for the database DDL: + +| Provider | Generated Default SQL | Behavior | +| :--- | :--- | :--- | +| **PostgreSQL** | `DEFAULT 'STUDENT'` | Coerced to `"UserRole"` enum type by Postgres. | +| **SQLite** | `DEFAULT 'STUDENT'` | Inserted as string default matching `TEXT` column. | + +In Go client builders (`Create()`), if an optional or defaulted enum field is left unspecified (`nil`), Phi automatically applies the schema default value during record map computation. + +--- + +## Access Pattern & Naming Conventions + +All enum types, constants, and validation methods live in the root generated package (`phi`): + +### Root Client Package (`phi`) +* **Enum Type**: `type UserRoleType string` +* **Enum Values**: `phi.UserRole_ADMIN`, `phi.UserRole_STUDENT`, `phi.UserRole_TEACHER` +* **Validation Method**: `(e UserRoleType).IsValid() bool` + +```go +role := phi.UserRole_ADMIN +if role.IsValid() { + fmt.Println("Role is valid:", role) +} +``` + +### Model Package (`user`) +The model package (e.g. `user`) exports predicate builders typed against `phi.UserRoleType`: +* **Field Predicates**: `user.Role` (type `phi.Field[phi.User, phi.UserRoleType]`) + +--- + +## Usage Examples + +### 1. Inserting Records (`Create` / `CreateMany`) + +Pass root enum constants directly to builder `Set*` methods or struct fields: + +```go +u, err := db.User.Create(). + SetEmail("student@example.com"). + SetPhoneNum("+1001"). + SetRole(phi.UserRole_STUDENT). + Exec(ctx) + +u2, err := db.User.Create(). + SetEmail("admin@example.com"). + SetPhoneNum("+1002"). + SetRole(phi.UserRole_ADMIN). + Exec(ctx) +``` + +### 2. Querying with Enum Predicates + +Filter records by enum values using `EQ()`, `NEQ()`, `In()`, and `NotIn()`: + +```go +// Find all Teachers or Admins +users, err := db.User.FindMany( + user.Or( + user.Role.EQ(phi.UserRole_TEACHER), + user.Role.EQ(phi.UserRole_ADMIN), + ), +).Exec(ctx) + +// Using In operator +students, err := db.User.FindMany( + user.Role.In([]phi.UserRoleType{phi.UserRole_STUDENT}), +).Exec(ctx) +``` + +### 3. Updating Enum Fields + +Update enum values on existing records: + +```go +updatedUser, err := db.User.Update(user.Id.EQ("user-123")). + SetRole(phi.UserRole_ADMIN). + Exec(ctx) +``` + +### 4. Conflict Updates in Upserts + +Set or update enum values during `OnConflict` upsert execution: + +```go +affected, err := db.User.CreateMany( + db.User.Create().SetEmail("user@example.com").SetPhoneNum("+1000").SetRole(phi.UserRole_STUDENT), +).OnConflict(user.Email).Update(func(u *phi.UserUpsert) { + u.Role.Set(phi.UserRole_ADMIN) +}).Exec(ctx) +``` diff --git a/docs/operations-and-types/Errors.md b/docs/operations-and-types/Errors.md new file mode 100644 index 0000000..c9f5100 --- /dev/null +++ b/docs/operations-and-types/Errors.md @@ -0,0 +1,147 @@ +--- +title: Error Handling +description: Sentinel errors, driver error translation, constraint inspection, and client-side validation rules in Phi. +category: Errors +tags: [errors, validation, constraints, translation, postgresql, sqlite, sentinel] +--- + +# Error Handling & Validation + +Phi features a robust, two-tiered error architecture: + +1. **Normalized Domain & Database Errors**: Standardized sentinel errors and inspection helpers that mask vendor differences across PostgreSQL and SQLite. +2. **Client-Side Validation**: Pre-query in-memory checks that catch malformed strings, invalid UUIDs, out-of-bounds numbers, and bad JSON before contacting the database. + +--- + +## 1. Domain Sentinel Errors & Inspectors + +Phi normalizes database errors into domain sentinel errors. Inspect them using `errors.Is(err, target)` or Ent-style inspector functions: + +| Sentinel Error | Ent-Style Inspector | Description | +| :------------------------- | :----------------------------- | :-------------------------------------------------------------- | +| `phi.ErrNotFound` | `phi.IsNotFound(err)` | Query expected a record but found none (`sql.ErrNoRows`). | +| `phi.ErrNoRowsAffected` | `phi.IsNoRowsAffected(err)` | Update or delete operation affected 0 rows. | +| `phi.ErrConstraint` | `phi.IsConstraintError(err)` | Base error for any database constraint violation. | +| `phi.ErrUniqueConstraint` | `phi.IsUniqueConstraint(err)` | Unique index or primary key collision. | +| `phi.ErrFKConstraint` | `phi.IsFKConstraint(err)` | Foreign key reference violation. | +| `phi.ErrNotNullConstraint` | `phi.IsNotNullConstraint(err)` | NOT NULL column violation. | +| `phi.ErrCheckConstraint` | `phi.IsCheckConstraint(err)` | SQL `CHECK` constraint failure. | +| `phi.ErrDeadlock` | `phi.IsDeadlock(err)` | Database deadlock detected. | +| `phi.ErrLockTimeout` | `phi.IsLockTimeout(err)` | Lock wait timeout exceeded. | +| `phi.ErrSerialization` | `phi.IsSerialization(err)` | Transaction serialization / concurrent update conflict. | +| `phi.ErrTxDone` | `phi.IsTxDone(err)` | Operation attempted on completed transaction (`sql.ErrTxDone`). | +| `phi.ErrConnClosed` | `phi.IsConnClosed(err)` | Database connection closed (`sql.ErrConnDone`). | + +### Usage Example + +```go +u, err := db.User.FindUnique(user.Email.EQ("user@example.com")).Exec(ctx) +if phi.IsNotFound(err) { + // Return HTTP 404 Not Found + return +} + +if phi.IsUniqueConstraint(err) { + // Return HTTP 409 Conflict + return +} +``` + +--- + +## 2. Concrete Error Structs + +For advanced debugging, Phi wraps errors in detailed concrete structs: + +### `*phi.NotFoundError` + +Carries the target model name alongside the underlying cause: + +```go +var nf *phi.NotFoundError +if errors.As(err, &nf) { + fmt.Printf("Model %s was not found: %v\n", nf.Model, nf.Cause) +} +``` + +### `*phi.ConstraintError` + +Carries the constraint kind, table name, constraint identifier, and driver cause: + +```go +var ce *phi.ConstraintError +if errors.As(err, &ce) { + fmt.Printf("Constraint %q violated on table %q (Kind: %v)\n", ce.Constraint, ce.Table, ce.Kind) +} +``` + +--- + +## 3. Driver Translation Engine (`TranslateDBError`) + +Phi normalizes database driver errors via `TranslateDBError(err)`: + +### Idempotency + +If `err` is already a normalized Phi domain error, `TranslateDBError` returns it unchanged to prevent redundant error wrapping. + +### Dialect Mapping Rules + +#### Standard Library (`database/sql`) + +- `sql.ErrNoRows` -> `phi.ErrNotFound` +- `sql.ErrTxDone` -> `phi.ErrTxDone` +- `sql.ErrConnDone` -> `phi.ErrConnClosed` + +#### PostgreSQL (`*pq.Error` & `SQLState`) + +- Code `23505` -> `ErrUniqueConstraint` +- Code `23503` -> `ErrFKConstraint` +- Code `23502` -> `ErrNotNullConstraint` +- Code `23514` -> `ErrCheckConstraint` +- Code `40001` -> `ErrSerialization` +- Code `40P01` -> `ErrDeadlock` + +#### SQLite (`ExtendedCode` & `Code`) + +- Extended Codes `2067`, `1555` -> `ErrUniqueConstraint` +- Extended Code `787` -> `ErrFKConstraint` +- Extended Code `1299` -> `ErrNotNullConstraint` +- Extended Code `275` -> `ErrCheckConstraint` + +--- + +## 4. Client-Side Validation (`ValidationError`) + +Phi runs in-memory validation on query inputs **before** making database roundtrips. + +### Inspecting Validation Errors + +Use `phi.IsValidationError(err)` or `errors.As`: + +```go +_, err := db.User.Create().SetEmail("invalid\x00user").Exec(ctx) +if phi.IsValidationError(err) { + var ve *phi.ValidationError + if errors.As(err, &ve) { + for _, fe := range ve.Errors { + fmt.Printf("Field: %s, Rule: %s, Message: %s\n", fe.Field, fe.Rule, fe.Msg) + } + } +} +``` + +### Built-in Validation Rules + +| Field Type | Rule | Validation Description | +| :---------------- | :------- | :--------------------------------------------------------------------------------------------------- | +| **String** | `safety` | Rejects strings containing null bytes (`\x00`) or invalid UTF-8 sequences. | +| **String** | `length` | Enforces `@db.VarChar(n)` maximum rune limits. | +| **UUID** | `format` | Validates standard 36-character UUID regex syntax. | +| **Decimal** | `format` | Validates numeric string syntax and enforces scale precision. | +| **Float** | `range` | Rejects `NaN` and infinite (`Inf`) values. | +| **JSON** | `format` | Enforces `json.Valid(val)` syntax on `json.RawMessage`. | +| **Bit String** | `format` | Enforces string contains only `'0'` and `'1'`. | +| **Inet / CIDR** | `format` | Enforces valid IP or CIDR address syntax via Go `net` package. | +| **Integer Types** | `range` | Enforces range bounds (`SmallInt`: $-32768$ to $32767$, `TinyInt`: $-128$ to $127$, `Oid`: $\ge 0$). | diff --git a/docs/operations-and-types/Raw-SQL.md b/docs/operations-and-types/Raw-SQL.md new file mode 100644 index 0000000..ec9fab0 --- /dev/null +++ b/docs/operations-and-types/Raw-SQL.md @@ -0,0 +1,39 @@ +--- +title: Raw SQL +description: Accessing standard database/sql handles for custom queries and vendor commands. +category: Operations +tags: [raw-sql, sql-db, sql-tx, fallback, database-handle] +--- + +# Raw SQL Fallback + +When you need to execute complex custom SQL, vendor-specific functions, or raw database commands, Phi provides direct access to Go's standard `database/sql` driver handles. + +--- + +## 1. Accessing `*sql.DB` (`db.Raw()`) + +Call `db.Raw()` on your client instance to retrieve the underlying `*sql.DB` handle: + +```go +// Execute raw query directly on *sql.DB +rows, err := db.Raw().QueryContext(ctx, `SELECT count(*) FROM "User" WHERE active = $1`, true) +if err != nil { + return err +} +defer rows.Close() +``` + +--- + +## 2. Accessing `*sql.Tx` (`tx.Raw()`) + +Call `tx.Raw()` inside any transaction to retrieve the underlying `*sql.Tx` handle: + +```go +err := db.Transaction(ctx, func(tx *phi.Tx) error { + // Execute raw SQL on the active transaction handle + _, err := tx.Raw().ExecContext(ctx, `UPDATE "User" SET loginCount = loginCount + 1 WHERE id = $1`, userID) + return err +}) +``` diff --git a/docs/operations-and-types/Transactions.md b/docs/operations-and-types/Transactions.md new file mode 100644 index 0000000..ac6b891 --- /dev/null +++ b/docs/operations-and-types/Transactions.md @@ -0,0 +1,91 @@ +--- +title: Transactions +description: Closure-based transactions, automatic rollback, panic recovery, and manual transaction control. +category: Operations +tags: [transactions, rollback, panic-recovery, begin-tx, commit] +--- + +# Transactions + +Phi provides closure-based transactions with automatic rollback and panic recovery, as well as manual transaction handles. + +--- + +## 1. Closure-Based Transactions (`db.Transaction`) + +`db.Transaction(ctx, fn)` manages the entire lifecycle of a transaction automatically: + +```go +err := db.Transaction(ctx, func(tx *phi.Tx) error { + // 1. Create a user within the transaction + u, err := tx.User.Create(). + SetEmail("tx-user@example.com"). + SetPhoneNum("+1999"). + Exec(ctx) + if err != nil { + return err // Triggers automatic tx.Rollback() + } + + // 2. Create a post associated with the new user + _, err = tx.Post.Create(). + SetTitle("First Post"). + SetContent("Hello World"). + SetAuthorId(u.Id). + Exec(ctx) + if err != nil { + return err // Triggers automatic tx.Rollback() + } + + return nil // Triggers automatic tx.Commit() +}) +``` + +--- + +## 2. Lifecycle & Execution Rules + +1. **Automatic Begin**: Executes `db.BeginTx(ctx, nil)` to start the transaction. +2. **Commit on Success**: If `fn(tx)` returns `nil`, `tx.Commit()` is called automatically. +3. **Rollback on Error**: If `fn(tx)` returns a non-nil `error`, `tx.Rollback()` is automatically executed, and the original error is returned. +4. **Panic Protection & Recovery**: + If a panic occurs anywhere inside `fn(tx)`, Phi's internal `defer` block catches the panic, rolls back the transaction to prevent database lockups, and then re-throws (`repanic`) the original panic: + + ```go + // Internal Phi Panic-Safety Guard: + defer func() { + if p := recover(); p != nil { + _ = tx.Rollback() // Guarantees database rollback before repanicking + panic(p) // Re-throws original panic + } + }() + ``` + +--- + +## 3. Manual Transactions (`db.BeginTx`) + +For workflows requiring manual control across function boundaries: + +```go +// 1. Begin manual transaction +tx, err := db.BeginTx(ctx, nil) +if err != nil { + return err +} + +// 2. Execute operations on tx +u, err := tx.User.Create(). + SetEmail("manual-tx@example.com"). + SetPhoneNum("+2000"). + Exec(ctx) + +if err != nil { + _ = tx.Rollback() + return err +} + +// 3. Commit manually +if err := tx.Commit(); err != nil { + return err +} +``` diff --git a/docs/overview/Client-Lifecycle.md b/docs/overview/Client-Lifecycle.md new file mode 100644 index 0000000..1ff477f --- /dev/null +++ b/docs/overview/Client-Lifecycle.md @@ -0,0 +1,107 @@ +--- +title: Client Lifecycle +description: Connection pooling, statement caching, extension registration, and graceful shutdown in Phi. +category: Overview +tags: [client, lifecycle, connection-pool, caching, open, close] +--- + +# Client Lifecycle + +This guide covers initializing the Phi client, configuring connection pool bounds, registering extension hooks during application startup, statement caching, and executing graceful shutdowns. + +--- + +## 1. Opening a Client Connection + +Initialize the generated client using `phi.Open(driverName, dataSourceName)`: + +```go +package main + +import ( + "log" + _ "github.com/lib/pq" // Import SQL driver + "myproject/phi" +) + +func main() { + // Open connection pool + db, err := phi.Open("postgres", "postgres://user:pass@localhost:5432/dbname?sslmode=disable") + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() // Guarantee clean shutdown +} +``` + +--- + +## 2. Connection Pool Configuration + +`db.Raw()` returns the underlying `*sql.DB` handle. Use it during startup to configure standard Go database connection pool parameters: + +```go +db.Raw().SetMaxOpenConns(25) +db.Raw().SetMaxIdleConns(5) +db.Raw().SetConnMaxLifetime(5 * time.Minute) +db.Raw().SetConnMaxIdleTime(1 * time.Minute) +``` + +--- + +## 3. Registering Extension Hooks + +Register middleware extensions (`.Use(...)`) during application initialization **before** serving queries. Registered extensions are stored in thread-safe delegates and inherited by transactions: + +```go +func initDB() (*phi.DB, error) { + db, err := phi.Open("sqlite3", "file:app.db?cache=shared&_pragma=foreign_keys(1)") + if err != nil { + return nil, err + } + + // Register global audit hook on user delegate + db.User.Use(user.Extension{ + Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) { + args.Data.Email = strings.ToLower(args.Data.Email) + return next(ctx, args) + }, + }) + + return db, nil +} +``` + +--- + +## 4. Prepared Statement Caching + +Phi includes a thread-safe internal prepared statement cache (`stmtCache`). Frequently executed query shapes reuse prepared statements automatically, minimizing database parsing overhead and boosting throughput. + +--- + +## 5. Graceful Shutdown (`db.Close()`) + +When your application terminates, call `db.Close()` to flush prepared statement caches, release active database connections, and shut down the connection pool cleanly: + +```go +func main() { + db, err := phi.Open("postgres", connString) + if err != nil { + log.Fatal(err) + } + + // Graceful shutdown on OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigChan + log.Println("Shutting down database client...") + if err := db.Close(); err != nil { + log.Printf("Error closing database connection: %v", err) + } + os.Exit(0) + }() +} +``` diff --git a/docs/overview/Introduction.md b/docs/overview/Introduction.md new file mode 100644 index 0000000..7349af2 --- /dev/null +++ b/docs/overview/Introduction.md @@ -0,0 +1,39 @@ +--- +title: Introduction +description: Overview, architecture, and design philosophy of the Phi Go ORM framework. +category: Overview +tags: [introduction, overview, architecture, prisma, go-orm] +--- + +# Introduction + +**Phi** is a high-performance, type-safe ORM framework for Go. Inspired by Prisma, Phi uses your `schema.prisma` file as the single source of truth to generate strongly-typed Go clients, database migrations, and index-optimized SQL queries. + +--- + +## Core Features & Philosophy + +### 1. Prisma Schema as Single Source of Truth +Define your data models, enums, indices, and relation constraints in standard Prisma Schema language (`schema.prisma`). Phi parses your schema and generates all Go structs, field helpers, and DDL migrations automatically. + +### 2. 100% Compile-Time Type Safety +Say goodbye to string-based column names and dynamic maps in query logic. Phi generates explicit field accessors for every model attribute: + +```go +// Fully type-safe builder: invalid field types or misspelled columns fail at compile-time! +users, err := db.User.FindMany( + user.Role.EQ(phi.UserRole_ADMIN), + user.LoginCount.GTE(10), +).Exec(ctx) +``` + +### 3. Zero-Allocation Query Building +Phi is built from the ground up for speed. Query builders construct AST nodes directly into SQL strings without runtime reflection or heavy memory allocations during query assembly. + +### 4. Dual Dialect Support (PostgreSQL & SQLite) +Write one unified Go codebase that works seamlessly across **PostgreSQL** and **SQLite**. Phi handles dialect differences (such as native PostgreSQL ENUMs vs. SQLite `TEXT` + `CHECK` constraints) transparently. + +### 5. Production-Ready Tooling +* **Extension Hooks**: Middleware hooks for intercepting, mutating, or caching queries (`Create`, `Read`, `Update`, `Delete`). +* **Transactions**: Closure-based transactions with panic recovery and automatic rollback. +* **Forward-Only Migrations**: Goose-compatible migration generator powered by the Atlas DDL engine. diff --git a/docs/overview/Limitations.md b/docs/overview/Limitations.md new file mode 100644 index 0000000..6d36798 --- /dev/null +++ b/docs/overview/Limitations.md @@ -0,0 +1,99 @@ +--- +title: Current Limitations +description: Known boundaries and architectural scope of Phi. +category: Overview +tags: [limitations, roadmap, nested-creates, joins, aggregations] +--- + +# Current Limitations + +Phi focuses on high-performance, zero-allocation Prisma-style query building and code generation. To maintain speed, clarity, and predictable SQL generation, certain features are currently out of scope or planned for future releases. + +--- + +## 1. No Inline Nested Creates (For Now) + +Phi does not currently support nested inline record creation inside a single `Create()` builder call (e.g. creating a parent record and its children in one nested builder input). + +### Current Pattern: + +Use sequential creation inside a closure transaction: + +```go +err := db.Transaction(ctx, func(tx *phi.Tx) error { + user, err := tx.User.Create().SetEmail("user@example.com").Exec(ctx) + if err != nil { + return err + } + + _, err = tx.Post.Create().SetTitle("First Post").SetAuthorId(user.Id).Exec(ctx) + return err +}) +``` + +--- + +## 2. No Inline Nested Relation Edges Update (For Now) + +Updating foreign key relation links or edges directly through nested relation mutations (such as inline `connect`, `disconnect`, or `update` on relation fields) is not currently supported in `Update()` builders. + +### Current Pattern: + +Update foreign key fields directly or execute updates on target relation models: + +```go +// Update foreign key column directly +updatedPost, err := db.Post.Update(post.Id.EQ("post-1")). + SetAuthorId("new-user-id"). + Exec(ctx) +``` + +--- + +## 3. No Arbitrary SQL Joins (For Now) + +Phi does not expose an explicit SQL `.Join()` query builder API. + +### Relation Preloading Strategy: + +Phi uses Prisma-style **2-pass batch selection** for loading relations: + +1. Primary records are queried using clean, index-optimized SQL predicates. +2. Selected relations (`args.Select.Posts = post.Query()`) are fetched via secondary batched `IN (...)` queries and linked in memory. + +If your use case requires custom multi-table SQL `JOIN` aggregations, use the raw SQL fallback (`db.Raw()`). + +--- + +## 4. Aggregations & GroupBy + +Phi provides `.Count()` for total record counts. Complex aggregations (`SUM`, `AVG`, `MIN`, `MAX`) and `GROUP BY / HAVING` queries are not built into the builder DSL. + +### Current Pattern: + +Use `db.Raw()` for custom aggregations: + +```go +var total int64 +err := db.Raw().QueryRowContext(ctx, `SELECT SUM(loginCount) FROM "User"`).Scan(&total) +``` + +--- + +## 5. Single-Level Relation Preloading + +Relation selection (`args.Select.Posts = post.Query()`) preloads immediate 1-level relations. Deep multi-level nested graph preloading (e.g. `User` -> `Posts` -> `Comments`) is not currently exposed in a single builder tree. + +--- + +--- + +## 7. Compile-Time Type Safety & Raw Expression Boundaries + +Phi intentionally restricts passing raw, un-sanitized SQL string snippets into `.Where(...)` predicate chains. Every builder predicate is strongly typed to its model column to guarantee compile-time type safety and prevent SQL injection vulnerabilities. + +If your query requires custom raw SQL expressions (such as complex subqueries or custom `WHERE` clauses), execute them using the raw SQL fallback: + +```go +rows, err := db.Raw().QueryContext(ctx, `SELECT * FROM "User" WHERE custom_func(loginCount) = $1`, 5) +``` diff --git a/docs/Quickstart.md b/docs/overview/Quickstart.md similarity index 83% rename from docs/Quickstart.md rename to docs/overview/Quickstart.md index 9f75d01..5ae501a 100644 --- a/docs/Quickstart.md +++ b/docs/overview/Quickstart.md @@ -1,10 +1,8 @@ --- title: Quickstart description: Install the Phi CLI, create a project, define a schema, migrate, and generate the type-safe Go ORM client. -category: quickstart +category: Overview tags: [quickstart, install, setup] -categoryOrder: 1 - --- # Phi Documentation @@ -59,19 +57,19 @@ Example `phi.yml`: ```yaml database: - url_env: DATABASE_URL # Environment variable for the client connection. - direct_url_env: DATABASE_DIRECT_URL # Environment variable used for migrations. + url_env: DATABASE_URL # Environment variable for the client connection. + direct_url_env: DATABASE_DIRECT_URL # Environment variable used for migrations. -schema: ./schema.prisma # Path to your Prisma schema. +schema: ./schema.prisma # Path to your Prisma schema. -client_name: phi # Name of the generated Go package. +client_name: phi # Name of the generated Go package. output: - client: ./phi - migrations: ./phi/migrations # Keep this inside the client directory if you plan to embed migrations. + client: ./phi + migrations: ./phi/migrations # Keep this inside the client directory if you plan to embed migrations. log: - - none # Available: none, all, query, warn, error + - none # Available: none, all, query, warn, error ``` --- @@ -115,9 +113,9 @@ phi -m This command will: -* Create the database if it doesn't exist. -* Generate a migration. -* Apply the migration. +- Create the database if it doesn't exist. +- Generate a migration. +- Apply the migration. --- diff --git a/docs/overview/Schema.md b/docs/overview/Schema.md new file mode 100644 index 0000000..c720456 --- /dev/null +++ b/docs/overview/Schema.md @@ -0,0 +1,136 @@ +--- +title: Schema Walkthrough +description: Prisma schema definitions, supported scalar/native types, and dialect mapping rules in Phi. +category: Schema & Types +tags: [schema, prisma, types, postgresql, sqlite, native-types, element-type] +--- + +# Schema Walkthrough & Types + +Phi uses Prisma Schema language (`schema.prisma`) to define data models, relationships, field constraints, and database providers. + +--- + +## 1. Schema Definition Walkthrough + +A minimal `schema.prisma` file consists of a **datasource** block and one or more **models**: + +```prisma +datasource db { + provider = "postgres" +} + +model User { + id String @id @default(cuid()) + email String @unique + phoneNum String @unique + role UserRole @default(STUDENT) + loginCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + posts Post[] + + @@unique([email, phoneNum]) +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) +} + +enum UserRole { + ADMIN + STUDENT + TEACHER +} +``` + +--- + +## 2. Supported Scalar Types & Go Mappings + +| Prisma Scalar Type | Go Client Type | Struct Field (Required) | Struct Field (Optional `?`) | +| :----------------- | :----------------- | :---------------------- | :-------------------------- | +| `String` | `string` | `string` | `*string` | +| `Boolean` | `bool` | `bool` | `*bool` | +| `Int` | `int32` | `int32` | `*int32` | +| `BigInt` | `int64` | `int64` | `*int64` | +| `Float` | `float64` | `float64` | `*float64` | +| `Decimal` | `string` | `string` | `*string` | +| `DateTime` | `time.Time` | `time.Time` | `*time.Time` | +| `Json` | `json.RawMessage` | `json.RawMessage` | `*json.RawMessage` | +| `Bytes` | `[]byte` | `[]byte` | `*[]byte` | +| `Enum` | `phi.UserRoleType` | `phi.UserRoleType` | `*phi.UserRoleType` | + +--- + +## 3. Native Attributes (`@db.*`) & Provider Mappings + +Phi adheres strictly to Prisma's provider-specific feature specifications: + +### PostgreSQL vs. SQLite Column Type Mapping + +| Prisma Definition | PostgreSQL SQL DDL | SQLite SQL DDL | +| :---------------------------------------- | :------------------------------ | :---------------------------------- | +| `id String @id @default(cuid())` | `TEXT NOT NULL` | `TEXT NOT NULL` | +| `id String @id @default(uuid())` | `TEXT` or `UUID` | `TEXT NOT NULL` | +| `field String @db.VarChar(255)` | `VARCHAR(255)` | Not supported by Prisma schema | +| `field String @db.Text` | `TEXT` | Not supported by Prisma schema | +| `field Int @id @default(autoincrement())` | `SERIAL` / `BIGSERIAL` | `INTEGER PRIMARY KEY AUTOINCREMENT` | +| `createdAt DateTime @default(now())` | `TIMESTAMP` | `TIMESTAMP` | +| `data Json` | `JSONB` | `TEXT` / `BLOB` | +| `role UserRole` | Native `CREATE TYPE "UserRole"` | `TEXT` + `CHECK ("role" IN (...))` | + +--- + +## 4. Scalar Array Fields in SQLite (`/// @elementType`) + +PostgreSQL natively supports scalar array types (e.g. `String[]` -> `text[]`). Because SQLite does not natively support scalar array types, Prisma schema restricts `String[]` on SQLite providers. + +To use typed scalar arrays in SQLite models, Phi supports the `/// @elementType ` doc-comment decorator placed above a `Json` field defaulting to `"[]"`: + +```prisma +model User { + id String @id @default(cuid()) + + /// @elementType String + tags Json @default("[]") +} +``` + +- **Go Client Generation**: Phi generates `Tags []string` on the model struct and handles JSON serialization and deserialization transparently. +- **SQLite DDL**: Generated as a `TEXT` or `BLOB` JSON column. + +--- + +## 5. SQL Indexes (`@@index`) + +Phi supports single-column and multi-column index declarations in `schema.prisma`: + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + role UserRole @default(STUDENT) + createdAt DateTime @default(now()) + + @@index([email]) + @@index([role, createdAt]) +} +``` + +### Migration DDL Generation +When `phi migrate` is executed, index definitions are calculated by the migration engine and output as standard DDL statements: + +```sql +CREATE INDEX "User_email_idx" ON "User" ("email"); +CREATE INDEX "User_role_createdAt_idx" ON "User" ("role", "createdAt"); +``` + +### Query Performance vs Unique Predicates +Non-unique `@@index` declarations speed up query execution (`FindMany`, `FindFirst`, `Count`) directly on the database engine. Because non-unique indexes can match multiple rows, they optimize SQL performance without generating `FindUnique` predicate handles (only `@id`, `@unique`, `@@id`, and `@@unique` generate `FindUnique` targets). diff --git a/docs/Cursor.md b/docs/pagination-and-sort/Cursor.md similarity index 98% rename from docs/Cursor.md rename to docs/pagination-and-sort/Cursor.md index 0ded3b4..d0d1348 100644 --- a/docs/Cursor.md +++ b/docs/pagination-and-sort/Cursor.md @@ -3,8 +3,6 @@ title: Cursor description: Fetch pages of results relative to a known record, identified by a unique predicate. category: Pagination & Sort tags: [cursor, pagination, keyset] -categoryOrder: 5 -order: 4 --- # Cursor diff --git a/docs/OrderBy.md b/docs/pagination-and-sort/OrderBy.md similarity index 98% rename from docs/OrderBy.md rename to docs/pagination-and-sort/OrderBy.md index aa88a5f..a5879dd 100644 --- a/docs/OrderBy.md +++ b/docs/pagination-and-sort/OrderBy.md @@ -3,8 +3,6 @@ title: Order By description: Sort query results using Asc or Desc on any scalar field. Mirrors SQL's ORDER BY. category: Pagination & Sort tags: [order, sort, asc, desc] -categoryOrder: 5 -order: 3 --- # Order By diff --git a/docs/Skip.md b/docs/pagination-and-sort/Skip.md similarity index 98% rename from docs/Skip.md rename to docs/pagination-and-sort/Skip.md index 0b28411..dacb261 100644 --- a/docs/Skip.md +++ b/docs/pagination-and-sort/Skip.md @@ -3,8 +3,6 @@ title: Skip description: Skip a number of matching records before returning results. Mirrors SQL's OFFSET. category: Pagination & Sort tags: [skip, offset, pagination] -categoryOrder: 5 -order: 2 --- # Skip diff --git a/docs/Take.md b/docs/pagination-and-sort/Take.md similarity index 98% rename from docs/Take.md rename to docs/pagination-and-sort/Take.md index 3d86bfc..71e31b8 100644 --- a/docs/Take.md +++ b/docs/pagination-and-sort/Take.md @@ -3,8 +3,6 @@ title: Take description: Limit the number of records returned by a query. Mirrors SQL's LIMIT. category: Pagination & Sort tags: [take, limit, pagination] -categoryOrder: 5 -order: 1 --- # Take diff --git a/docs/sidebar-manifest.json b/docs/sidebar-manifest.json new file mode 100644 index 0000000..388505c --- /dev/null +++ b/docs/sidebar-manifest.json @@ -0,0 +1,38 @@ +{ + "categories": [ + { + "name": "Overview", + "files": ["Introduction", "Quickstart", "Schema", "Client-Lifecycle", "Limitations"] + }, + { + "name": "CLI", + "files": ["Init-Command", "Client-Generation", "Migrations"] + }, + { + "name": "CRUD", + "files": [ + "Create-Records", + "Read-Records", + "Update-Records", + "Delete-Records", + "Upsert-Records", + "Count-Records", + "Select", + "Omit", + "Query-Filters" + ] + }, + { + "name": "Hooks", + "files": ["Create-Hooks", "Read-Hooks", "Update-Hooks", "Delete-Hooks"] + }, + { + "name": "Pagination & Sort", + "files": ["Take", "Skip", "OrderBy", "Cursor"] + }, + { + "name": "Operations & Types", + "files": ["Enums", "Composite-Keys", "Transactions", "Raw-SQL", "Errors"] + } + ] +} diff --git a/docs/template.md b/docs/template.md index 9115876..50e7de9 100644 --- a/docs/template.md +++ b/docs/template.md @@ -3,8 +3,6 @@ title: Page Title description: A short summary of this guide (used in search results and meta description). category: Documentation tags: [read, query, findUnique, findFirst, findMany, select, orm] -categoryOrder: 200 # used for sorting the category among others -order: 2 # used for sorting the guide inside it's category --- # Page Title