fraiseql_cli/cli.rs
1//! CLI argument definitions: `Cli` struct, `Commands` enum, and all sub-command enums.
2
3use clap::{Parser, Subcommand};
4
5/// Exit codes documented in help text
6pub(crate) const EXIT_CODES_HELP: &str = "\
7EXIT CODES:
8 0 Success - Command completed successfully
9 1 Error - Command failed with an error
10 2 Validation failed - Schema or input validation failed";
11
12/// FraiseQL CLI - Compile GraphQL schemas to optimized SQL execution
13#[derive(Parser)]
14#[command(name = "fraiseql")]
15#[command(author, version, about, long_about = None)]
16#[command(propagate_version = true)]
17#[command(after_help = EXIT_CODES_HELP)]
18pub(crate) struct Cli {
19 /// Enable verbose logging
20 #[arg(short, long, global = true)]
21 pub(crate) verbose: bool,
22
23 /// Enable debug logging
24 #[arg(short, long, global = true)]
25 pub(crate) debug: bool,
26
27 /// Output as JSON (machine-readable)
28 #[arg(long, global = true)]
29 pub(crate) json: bool,
30
31 /// Suppress output (exit code only)
32 #[arg(short, long, global = true)]
33 pub(crate) quiet: bool,
34
35 #[command(subcommand)]
36 pub(crate) command: Commands,
37}
38
39#[derive(Subcommand)]
40pub(crate) enum Commands {
41 /// Compile schema to optimized schema.compiled.json
42 ///
43 /// Supports three workflows:
44 /// 1. TOML-only: fraiseql compile fraiseql.toml
45 /// 2. Language + TOML: fraiseql compile fraiseql.toml --types types.json
46 /// 3. Legacy JSON: fraiseql compile schema.json
47 #[command(after_help = "\
48EXAMPLES:
49 fraiseql compile fraiseql.toml
50 fraiseql compile fraiseql.toml --types types.json
51 fraiseql compile schema.json -o schema.compiled.json
52 fraiseql compile fraiseql.toml --check")]
53 Compile {
54 /// Input file path: fraiseql.toml (TOML) or schema.json (legacy)
55 #[arg(value_name = "INPUT")]
56 input: String,
57
58 /// Optional types.json from language implementation (used with fraiseql.toml)
59 #[arg(long, value_name = "TYPES")]
60 types: Option<String>,
61
62 /// Directory for auto-discovery of schema files (recursive *.json)
63 #[arg(long, value_name = "DIR")]
64 schema_dir: Option<String>,
65
66 /// Type files (repeatable): fraiseql compile fraiseql.toml --type-file a.json --type-file
67 /// b.json
68 #[arg(long = "type-file", value_name = "FILE")]
69 type_files: Vec<String>,
70
71 /// Query files (repeatable)
72 #[arg(long = "query-file", value_name = "FILE")]
73 query_files: Vec<String>,
74
75 /// Mutation files (repeatable)
76 #[arg(long = "mutation-file", value_name = "FILE")]
77 mutation_files: Vec<String>,
78
79 /// Output schema.compiled.json file path
80 #[arg(
81 short,
82 long,
83 value_name = "OUTPUT",
84 default_value = "schema.compiled.json"
85 )]
86 output: String,
87
88 /// Validate only, don't write output
89 #[arg(long)]
90 check: bool,
91
92 /// Skip embedding content hash in compiled schema (for test fixtures)
93 #[arg(long)]
94 skip_hash: bool,
95
96 /// Optional database URL: check the schema against the live database
97 ///
98 /// Validates each `sql_source`, mutation function, JSONB key and query argument
99 /// against the relations it names, and fills `native_columns` from the views'
100 /// columns. Drift classified as an error fails the compile (see `--allow-drift`).
101 #[arg(long, value_name = "DATABASE_URL")]
102 database: Option<String>,
103
104 /// Emit DDL files for all schema types to the given directory
105 ///
106 /// Writes `CREATE TABLE` DDL for each compiled type to `<DIR>/<type>.sql`.
107 /// Output is compatible with confiture's `db/schema/` directory format,
108 /// enabling `fraiseql migrate generate` to auto-detect schema drift.
109 #[arg(long, value_name = "DIR")]
110 emit_ddl: Option<String>,
111
112 /// Check compiled schema against the database for migration drift
113 ///
114 /// Emits DDL to a temporary directory, then delegates to
115 /// `confiture migrate validate` for drift detection.
116 /// Exits non-zero when the compiled schema diverges from the database.
117 #[arg(long)]
118 check_migrations: bool,
119
120 /// Compile despite error-severity schema↔database drift (advisory mode)
121 ///
122 /// With `--database`, drift the linter classifies as an error (a
123 /// `sql_source` that does not exist, a required field's JSONB key
124 /// absent from every sampled row, a mutation with no backing function)
125 /// fails the compile. This flag downgrades those findings to reported
126 /// advisories and writes the artifact anyway.
127 #[arg(long, requires = "database")]
128 allow_drift: bool,
129
130 /// Project config to compile with. Default: the nearest fraiseql.toml beside
131 /// or above the input, up to the repository root — never the working directory
132 #[arg(long, value_name = "PATH")]
133 config: Option<String>,
134 },
135
136 /// Extract schema from annotated source files
137 ///
138 /// Parses FraiseQL annotations in any supported language and generates schema.json.
139 /// No language runtime required — pure text processing.
140 #[command(after_help = "\
141EXAMPLES:
142 fraiseql extract schema/schema.py
143 fraiseql extract schema/ --recursive
144 fraiseql extract schema.rs --language rust -o schema.json")]
145 Extract {
146 /// Source file(s) or directory to extract from
147 #[arg(value_name = "INPUT")]
148 input: Vec<String>,
149
150 /// Override language detection (python, typescript, rust, java, kotlin, go, csharp, swift,
151 /// scala)
152 #[arg(short, long)]
153 language: Option<String>,
154
155 /// Recursively scan directories
156 #[arg(short, long)]
157 recursive: bool,
158
159 /// Output file path
160 #[arg(short, long, default_value = "schema.json")]
161 output: String,
162 },
163
164 /// Explain query execution plan and complexity
165 ///
166 /// Shows GraphQL query execution plan, SQL, and complexity analysis.
167 #[command(after_help = "\
168EXAMPLES:
169 fraiseql explain '{ users { id name } }'
170 fraiseql explain '{ user(id: 1) { posts { title } } }' --json")]
171 Explain {
172 /// GraphQL query string
173 #[arg(value_name = "QUERY")]
174 query: String,
175 },
176
177 /// Calculate query complexity score
178 ///
179 /// Quick analysis of query complexity (depth, field count, score).
180 #[command(after_help = "\
181EXAMPLES:
182 fraiseql cost '{ users { id name } }'
183 fraiseql cost '{ deeply { nested { query { here } } } }' --json")]
184 Cost {
185 /// GraphQL query string
186 #[arg(value_name = "QUERY")]
187 query: String,
188 },
189
190 /// Analyze schema for optimization opportunities
191 ///
192 /// Provides recommendations across 6 categories:
193 /// performance, security, federation, complexity, caching, indexing
194 #[command(after_help = "\
195EXAMPLES:
196 fraiseql analyze schema.compiled.json
197 fraiseql analyze schema.compiled.json --json")]
198 Analyze {
199 /// Path to schema.compiled.json
200 #[arg(value_name = "SCHEMA")]
201 schema: String,
202 },
203
204 /// Analyze schema type dependencies
205 ///
206 /// Exports dependency graph, detects cycles, and finds unused types.
207 /// Supports multiple output formats for visualization and CI integration.
208 #[command(after_help = "\
209EXAMPLES:
210 fraiseql dependency-graph schema.compiled.json
211 fraiseql dependency-graph schema.compiled.json -f dot > graph.dot
212 fraiseql dependency-graph schema.compiled.json -f mermaid
213 fraiseql dependency-graph schema.compiled.json --json")]
214 DependencyGraph {
215 /// Path to schema.compiled.json
216 #[arg(value_name = "SCHEMA")]
217 schema: String,
218
219 /// Output format (json, dot, mermaid, d2, console)
220 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
221 format: String,
222 },
223
224 /// Federation tools: draw the graph subgraphs form, check a subgraph
225 ///
226 /// `graph` takes each subgraph's compiled schema and draws who extends whose entities.
227 #[command(after_help = "\
228EXAMPLES:
229 fraiseql federation graph users.compiled.json orders.compiled.json
230 fraiseql federation graph users.compiled.json orders.compiled.json -f dot
231 fraiseql federation graph users.compiled.json orders.compiled.json -f mermaid
232 fraiseql federation check schema.compiled.json --against other.compiled.json
233 fraiseql federation sdl schema.compiled.json -o subgraph.graphql (--features federation)")]
234 Federation {
235 /// Schema path (positional argument passed to subcommand)
236 #[command(subcommand)]
237 command: FederationCommands,
238 },
239
240 /// Lint schema for FraiseQL design quality
241 ///
242 /// Analyzes schema using FraiseQL-calibrated design rules.
243 /// Detects JSONB batching issues, compilation problems, auth boundaries, etc.
244 #[command(after_help = "\
245EXAMPLES:
246 fraiseql lint schema.json
247 fraiseql lint schema.compiled.json --federation
248 fraiseql lint schema.json --fail-on-critical
249 fraiseql lint schema.json --json")]
250 Lint {
251 /// Path to schema.json or schema.compiled.json
252 #[arg(value_name = "SCHEMA")]
253 schema: String,
254
255 /// Only show federation audit
256 #[arg(long)]
257 federation: bool,
258
259 /// Only show cost audit
260 #[arg(long)]
261 cost: bool,
262
263 /// Only show cache audit
264 #[arg(long)]
265 cache: bool,
266
267 /// Only show auth audit
268 #[arg(long)]
269 auth: bool,
270
271 /// Only show compilation audit
272 #[arg(long)]
273 compilation: bool,
274
275 /// Exit with error if any critical issues found
276 #[arg(long)]
277 fail_on_critical: bool,
278
279 /// Exit with error if any warning or critical issues found
280 #[arg(long)]
281 fail_on_warning: bool,
282 },
283
284 /// Generate DDL for Arrow views (va_*, tv_*, ta_*)
285 #[command(after_help = "\
286EXAMPLES:
287 fraiseql generate-views -s schema.json -e User --view va_users
288 fraiseql generate-views -s schema.json -e Order --view tv_orders --refresh-strategy scheduled")]
289 GenerateViews {
290 /// Path to schema.json
291 #[arg(short, long, value_name = "SCHEMA")]
292 schema: String,
293
294 /// Entity name from schema
295 #[arg(short, long, value_name = "NAME")]
296 entity: String,
297
298 /// View name (must start with va_, tv_, or ta_)
299 #[arg(long, value_name = "NAME")]
300 view: String,
301
302 /// Refresh strategy (trigger-based or scheduled)
303 #[arg(long, value_name = "STRATEGY", default_value = "trigger-based")]
304 refresh_strategy: String,
305
306 /// Output file path (default: {view}.sql)
307 #[arg(short, long, value_name = "PATH")]
308 output: Option<String>,
309
310 /// Include helper/composition views
311 #[arg(long, default_value = "true")]
312 include_composition_views: bool,
313
314 /// Include monitoring functions
315 #[arg(long, default_value = "true")]
316 include_monitoring: bool,
317
318 /// Validate the generated DDL by executing it against PostgreSQL
319 /// (DATABASE_URL) in a rolled-back transaction; don't write a file
320 #[arg(long)]
321 validate: bool,
322
323 /// Show generation steps (use global --verbose flag)
324 #[arg(long, action = clap::ArgAction::SetTrue)]
325 gen_verbose: bool,
326 },
327
328 /// Generate external-write capture-trigger DDL for @subscribable types (#366)
329 ///
330 /// Emits a self-contained, idempotent PostgreSQL script (the capture function
331 /// plus per-table statement-level triggers) that brings raw external writes
332 /// (psql / migrations / third-party tools) to a `@subscribable` type's table(s)
333 /// onto the Change Spine, without double-emitting for FraiseQL-mediated writes.
334 #[command(after_help = "\
335EXAMPLES:
336 fraiseql generate-capture-triggers -s schema.compiled.json | psql \"$DATABASE_URL\"
337 fraiseql generate-capture-triggers -s schema.compiled.json -o capture.sql")]
338 GenerateCaptureTriggers {
339 /// Path to the compiled schema (schema.compiled.json)
340 #[arg(short, long, value_name = "SCHEMA")]
341 schema: String,
342
343 /// Output file path (default: stdout, for piping into psql)
344 #[arg(short, long, value_name = "PATH")]
345 output: Option<String>,
346
347 /// Omit the capture function definition (emit only the per-table triggers)
348 #[arg(long)]
349 no_function: bool,
350 },
351
352 /// Validate schema.json or fact tables
353 ///
354 /// Performs comprehensive schema validation including:
355 /// - JSON structure validation
356 /// - Type reference validation
357 /// - Circular dependency detection (with --check-cycles)
358 /// - Unused type detection (with --check-unused)
359 #[command(after_help = "\
360EXAMPLES:
361 fraiseql validate schema.json
362 fraiseql validate schema.json --check-unused
363 fraiseql validate schema.json --strict
364 fraiseql validate facts -s schema.json --database postgres://localhost/db")]
365 Validate {
366 #[command(subcommand)]
367 command: Option<ValidateCommands>,
368
369 /// Schema.json file path to validate (if no subcommand)
370 #[arg(value_name = "INPUT")]
371 input: Option<String>,
372
373 /// Check for circular dependencies between types
374 #[arg(long, default_value = "true")]
375 check_cycles: bool,
376
377 /// Check for unused types (no incoming references)
378 #[arg(long)]
379 check_unused: bool,
380
381 /// Strict mode: treat warnings as errors (unused types become errors)
382 #[arg(long)]
383 strict: bool,
384
385 /// Only analyze specific type(s) - comma-separated list
386 #[arg(long, value_name = "TYPES", value_delimiter = ',')]
387 types: Vec<String>,
388
389 /// Validate the mutation contract against a live PostgreSQL database.
390 ///
391 /// For each DB-backed mutation, checks that its `sql_source` function
392 /// exists with the argument layout the server sends and returns the
393 /// columns the server decodes into `MutationResponse`. Requires a
394 /// compiled schema (schema.compiled.json) as INPUT. (#397)
395 #[arg(long, value_name = "DATABASE_URL")]
396 against_db: Option<String>,
397 },
398
399 /// Introspect database for fact tables and output suggestions
400 #[command(after_help = "\
401EXAMPLES:
402 fraiseql introspect facts --database postgres://localhost/db
403 fraiseql introspect facts --database postgres://localhost/db -f json")]
404 Introspect {
405 #[command(subcommand)]
406 command: IntrospectCommands,
407 },
408
409 /// Generate authoring-language source from schema.json
410 ///
411 /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
412 /// source code in any of the 9 supported authoring languages.
413 #[command(after_help = "\
414EXAMPLES:
415 fraiseql generate schema.json --language python
416 fraiseql generate schema.json --language rust -o schema.rs
417 fraiseql generate schema.json --language typescript")]
418 Generate {
419 /// Path to schema.json
420 #[arg(value_name = "INPUT")]
421 input: String,
422
423 /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
424 #[arg(short, long)]
425 language: String,
426
427 /// Output file path (default: schema.<ext> based on language)
428 #[arg(short, long)]
429 output: Option<String>,
430 },
431
432 /// Generate consumer-side typed clients from a compiled schema
433 ///
434 /// Distinct from `generate`: `generate` emits server-side authoring code (write
435 /// FraiseQL types in another language, fed back into the compiler), whereas
436 /// `generate-client` emits a typed client for callers of a FraiseQL API
437 /// (interfaces + query/mutation functions) from a compiled schema.compiled.json.
438 #[command(after_help = "\
439EXAMPLES:
440 fraiseql generate-client typescript --out ./src/generated
441 fraiseql generate-client typescript --schema ./schema.compiled.json --out ./gen --force
442 fraiseql generate-client python --out ./app/fraiseql_client
443 fraiseql generate-client go --out ./internal/fraiseqlclient
444 fraiseql generate-client rust --out ./src/generated")]
445 GenerateClient {
446 #[command(subcommand)]
447 language: GenerateClientCommands,
448 },
449
450 /// Initialize a new FraiseQL project
451 ///
452 /// Creates project directory with fraiseql.toml, schema.json,
453 /// database DDL structure, and authoring skeleton.
454 #[command(after_help = "\
455EXAMPLES:
456 fraiseql init my-app
457 fraiseql init my-app --language typescript --database postgres
458 fraiseql init my-app --size xs --no-git")]
459 Init {
460 /// Project name (used as directory name)
461 #[arg(value_name = "PROJECT_NAME")]
462 project_name: String,
463
464 /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
465 #[arg(short, long, default_value = "python")]
466 language: String,
467
468 /// Target database (postgres — FraiseQL is PostgreSQL-only since v2.15.0)
469 #[arg(long, default_value = "postgres")]
470 database: String,
471
472 /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
473 #[arg(long, default_value = "s")]
474 size: String,
475
476 /// Skip git init
477 #[arg(long)]
478 no_git: bool,
479 },
480
481 /// Run database migrations
482 ///
483 /// The long help is `commands::migrate::MIGRATE_LONG_ABOUT`: it states the order the
484 /// database URL is resolved in, so it lives beside the resolver.
485 #[command(long_about = crate::commands::migrate::MIGRATE_LONG_ABOUT)]
486 #[command(after_help = "\
487EXAMPLES:
488 fraiseql migrate up --database postgres://localhost/mydb
489 fraiseql migrate down --steps 1
490 fraiseql migrate status
491 fraiseql migrate create add_posts_table")]
492 Migrate {
493 #[command(subcommand)]
494 command: MigrateCommands,
495 },
496
497 /// Generate Software Bill of Materials
498 ///
499 /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
500 #[command(after_help = "\
501EXAMPLES:
502 fraiseql sbom
503 fraiseql sbom --format spdx
504 fraiseql sbom --format cyclonedx --output sbom.json")]
505 Sbom {
506 /// Output format (cyclonedx, spdx)
507 #[arg(short, long, default_value = "cyclonedx")]
508 format: String,
509
510 /// Output file path (default: stdout)
511 #[arg(short, long, value_name = "FILE")]
512 output: Option<String>,
513 },
514
515 /// List every optional capability and whether this build has it
516 ///
517 /// Reports the server this binary links (the one `fraiseql run` starts): each
518 /// capability's maturity, whether it is compiled in, its Cargo feature, its config
519 /// keys and compiled-schema sections, the smallest published artifact that ships it,
520 /// and its documentation page.
521 #[cfg(feature = "run-server")]
522 #[command(after_help = "\
523EXAMPLES:
524 fraiseql capabilities
525 fraiseql capabilities --json")]
526 Capabilities,
527
528 /// Compile schema and immediately start the GraphQL server
529 ///
530 /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
531 /// With --watch, the server hot-reloads whenever the schema file changes.
532 ///
533 /// Server and database settings can be declared in fraiseql.toml under [server]
534 /// and [database] sections. CLI flags take precedence over TOML settings, which
535 /// take precedence over defaults. The database URL is resolved in this order:
536 /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
537 #[cfg(feature = "run-server")]
538 #[command(after_help = "\
539EXAMPLES:
540 fraiseql run
541 fraiseql run fraiseql.toml --database postgres://localhost/mydb
542 fraiseql run --port 3000 --watch
543 fraiseql run schema.json --introspection
544
545TOML CONFIG:
546 [server]
547 host = \"127.0.0.1\"
548 port = 9000
549
550 [server.cors]
551 origins = [\"https://app.example.com\"]
552
553 [database]
554 url = \"${DATABASE_URL}\"
555 pool_min = 2
556 pool_max = 20")]
557 Run {
558 /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
559 #[arg(value_name = "INPUT")]
560 input: Option<String>,
561
562 /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
563 // Long-only: the global `--debug` (`Cli.debug`) already owns `-d`. A local
564 // `-d`/`--database` short here collides with it — clap's debug_asserts panic in
565 // debug builds and the short is ambiguous in release builds (#650).
566 #[arg(long, value_name = "DATABASE_URL")]
567 database: Option<String>,
568
569 /// Port to listen on (overrides [server].port in fraiseql.toml)
570 #[arg(short, long, value_name = "PORT")]
571 port: Option<u16>,
572
573 /// Bind address (overrides [server].host in fraiseql.toml)
574 #[arg(long, value_name = "HOST")]
575 bind: Option<String>,
576
577 /// Watch input file for changes and hot-reload the server
578 #[arg(short, long)]
579 watch: bool,
580
581 /// Enable the GraphQL introspection endpoint (no auth required)
582 #[arg(long)]
583 introspection: bool,
584 },
585
586 /// Watch a schema source and recompile + live-reload a running server on change
587 ///
588 /// Recompiles `schema.compiled.json` on every save and, with `--reload-url`,
589 /// POSTs it to a running server's `/api/v1/admin/reload-schema` endpoint for a
590 /// zero-downtime `ArcSwap` reload (no restart). Unlike `run --watch`, this drives
591 /// a server you started separately.
592 Watch {
593 /// Schema source to watch (fraiseql.toml or schema.json)
594 #[arg(value_name = "INPUT")]
595 input: String,
596
597 /// Compiled-schema output path (the server reads this on reload)
598 #[arg(short, long, default_value = "schema.compiled.json")]
599 output: String,
600
601 /// Base URL of a running server to live-reload (e.g. http://localhost:8080).
602 /// Omit to recompile to disk only.
603 #[arg(long, value_name = "URL")]
604 reload_url: Option<String>,
605
606 /// Bearer token for the server's admin reload endpoint
607 #[arg(long, value_name = "TOKEN")]
608 admin_token: Option<String>,
609
610 /// Database URL for compile-time validation (overrides DATABASE_URL)
611 #[arg(long, value_name = "DATABASE_URL")]
612 database: Option<String>,
613 },
614
615 /// Validate a trusted documents manifest
616 ///
617 /// Checks that the manifest JSON is well-formed and that each key
618 /// is a valid SHA-256 hex string matching its query body.
619 #[command(after_help = "\
620EXAMPLES:
621 fraiseql validate-documents manifest.json
622 fraiseql validate-documents manifest.json --max-cost 1000 --schema schema.compiled.json")]
623 ValidateDocuments {
624 /// Path to the trusted documents manifest JSON file
625 #[arg(value_name = "MANIFEST")]
626 manifest: String,
627
628 /// Fail validation when any document's estimated operation cost exceeds
629 /// this ceiling (#379). Costs are worst-case: a variable-valued
630 /// pagination argument is scored at its fail-closed ceiling.
631 #[arg(long, value_name = "COST")]
632 max_cost: Option<u64>,
633
634 /// Compiled schema (`schema.compiled.json`) supplying the
635 /// `[fraiseql.cost_weights]` used to score documents — the same numbers
636 /// the runtime enforces. Without it, cost equals the complexity score.
637 #[arg(long, value_name = "SCHEMA")]
638 schema: Option<String>,
639 },
640
641 /// Install FraiseQL mutation helper functions
642 ///
643 /// Installs SQL helper functions (fraiseql.mutation_ok, fraiseql.mutation_err, etc.)
644 /// to reduce boilerplate when writing mutation functions under the v2.4.0 protocol.
645 /// The helpers are installed in the `fraiseql` schema, which is owned by FraiseQL's
646 /// database role.
647 #[command(after_help = "\
648EXAMPLES:
649 fraiseql setup --database postgres://localhost/mydb
650 fraiseql setup --dry-run
651 fraiseql setup # Uses [database].url from fraiseql.toml, else DATABASE_URL")]
652 Setup {
653 /// Database connection URL (else [database].url in fraiseql.toml, else the
654 /// DATABASE_URL env var)
655 #[arg(long, value_name = "DATABASE_URL")]
656 database: Option<String>,
657
658 /// Print SQL without applying changes
659 #[arg(long)]
660 dry_run: bool,
661 },
662
663 /// Inspect schema metadata from a running FraiseQL server
664 ///
665 /// Fetches field-level security metadata (encryption, scope requirements, deny actions)
666 /// from the server's `/api/v1/schema/metadata` endpoint and displays it as a table.
667 #[command(after_help = "\
668EXAMPLES:
669 fraiseql schema metadata
670 fraiseql schema metadata --server http://localhost:8080
671 fraiseql schema metadata --server https://api.example.com --token mytoken")]
672 Schema {
673 #[command(subcommand)]
674 command: SchemaCommands,
675 },
676
677 /// Execute one GraphQL operation against a database and print the JSON result
678 ///
679 /// Boots the compiled schema in-process (no long-lived server, no HTTP layer)
680 /// against PostgreSQL, runs a single query or mutation, prints the GraphQL JSON
681 /// response to stdout, and exits. Exits non-zero on a resolution error — a
682 /// scriptable "does this actually resolve?" check for CI and the inner loop.
683 ///
684 /// Mutations COMMIT unless `--dry-run` is given, which runs the mutation inside
685 /// a transaction that is rolled back (validate-bind-without-commit).
686 #[command(after_help = "\
687EXAMPLES:
688 fraiseql query '{ orders(limit: 1) { id } }'
689 fraiseql query --schema schema.compiled.json --database postgres://localhost/db '{ users { id } }'
690 fraiseql query --variables '{\"id\": 42}' 'query($id: Int!) { user(id: $id) { id } }'
691 fraiseql query --dry-run 'mutation { createUser(name: \"x\") { id } }'")]
692 Query {
693 /// GraphQL operation to execute (a single query or mutation document).
694 #[arg(value_name = "QUERY")]
695 query: String,
696
697 /// Path to schema.compiled.json.
698 #[arg(short = 's', long, default_value = "schema.compiled.json")]
699 schema: std::path::PathBuf,
700
701 /// Database URL (PostgreSQL only). Falls back to the DATABASE_URL env var.
702 #[arg(long, value_name = "DATABASE_URL")]
703 database: Option<String>,
704
705 /// GraphQL variables as a JSON object string (e.g. '{"id": 42}').
706 #[arg(long, value_name = "JSON")]
707 variables: Option<String>,
708
709 /// Run mutations inside a rolled-back transaction: the function binds and
710 /// executes (constraints, triggers, and response shape are validated) but
711 /// nothing is committed. No effect on queries. PostgreSQL only.
712 #[arg(long)]
713 dry_run: bool,
714 },
715
716 /// Run diagnostic checks for common FraiseQL setup problems
717 ///
718 /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
719 /// and cache/auth coherence. Prints a color-coded report and exits 0 if
720 /// all checks pass or 1 if any check fails (warnings do not fail).
721 #[command(after_help = "\
722EXAMPLES:
723 fraiseql doctor
724 fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
725 fraiseql doctor --db-url postgres://user:pass@host:5432/db
726 fraiseql doctor --runtime --against-db postgres://user:pass@host:5432/db
727 fraiseql doctor --json")]
728 Doctor {
729 /// Path to fraiseql.toml configuration file.
730 #[arg(long, default_value = "fraiseql.toml")]
731 config: std::path::PathBuf,
732
733 /// Path to schema.compiled.json.
734 #[arg(long, default_value = "schema.compiled.json")]
735 schema: std::path::PathBuf,
736
737 /// Override DATABASE_URL for the connectivity check.
738 #[arg(long)]
739 db_url: Option<String>,
740
741 /// Run the live-database passes against this PostgreSQL database: the
742 /// change-log contract drift check (compares core.tb_entity_change_log
743 /// against the shipped contract — #380), the PL/pgSQL body-resolution
744 /// pass (reports internal calls that no longer resolve — #409), the
745 /// mutation→function contract drift check (every declared mutation has a
746 /// matching backing function with a mutation_response-shaped return —
747 /// #384), and the view-drift check (the same L1/L2/L3 view-composition
748 /// linter `compile --database` runs, reported as structured checks —
749 /// #384).
750 ///
751 /// The body-resolution pass requires the `plpgsql_check` extension and
752 /// skips with a hint when it is unavailable.
753 #[arg(long, value_name = "DATABASE_URL")]
754 against_db: Option<String>,
755
756 /// Runtime smoke mode (#501): boot the compiled schema in-process and
757 /// probe each root query field with a minimal selection, dry-running each
758 /// no-argument mutation (rolled back). Confirms operations actually
759 /// *resolve* end-to-end, complementing the static `--against-db` drift
760 /// checks. Requires a PostgreSQL URL via `--against-db` (or `--db-url`).
761 /// Queries / mutations that require arguments are skipped with a warning.
762 #[arg(long)]
763 runtime: bool,
764
765 /// Schemas to scan in the body-resolution pass (comma-separated).
766 #[arg(
767 long,
768 value_name = "SCHEMAS",
769 value_delimiter = ',',
770 default_value = "public"
771 )]
772 schemas: Vec<String>,
773
774 /// Output machine-readable JSON (for CI integration).
775 #[arg(long)]
776 json: bool,
777 },
778
779 /// Change-log performance observability (#392)
780 ///
781 /// Reads the framework-owned change-log (`core.v_entity_change_log`) and turns
782 /// it into operator-facing forensics: latency regressions between two time
783 /// windows, and ad-hoc reads of the slowest mutations / duration completeness
784 /// / per-operation percentiles.
785 Perf {
786 #[command(subcommand)]
787 command: PerfCommands,
788 },
789
790 /// List scheduled ingress sources and their durable cursor state (#573)
791 ///
792 /// Reads the compiled schema for each source's schedule and `run_as` ceiling,
793 /// and — when a database is reachable — its cursor value, version, and staleness
794 /// (seconds since the last advance). Leadership and last-fire are not reported
795 /// here (an advisory lock has no visible holder, and the lease is released
796 /// between ticks): watch `fraiseql_source_*` metrics and the poller logs for
797 /// those. Always exits 0 — it is a report.
798 #[command(after_help = "\
799EXAMPLES:
800 fraiseql sources
801 fraiseql sources --schema schema.compiled.json
802 fraiseql sources --db-url postgres://user:pass@host:5432/db
803 fraiseql sources --json")]
804 Sources {
805 /// Path to schema.compiled.json.
806 #[arg(long, default_value = "schema.compiled.json")]
807 schema: std::path::PathBuf,
808
809 /// PostgreSQL URL for cursor reads (defaults to `fraiseql.toml` /
810 /// `DATABASE_URL`). Without one, definitions are listed with no cursor state.
811 #[arg(long)]
812 db_url: Option<String>,
813
814 /// Output machine-readable JSON.
815 #[arg(long)]
816 json: bool,
817 },
818
819 /// Author-side function tooling (local test harness).
820 #[cfg(feature = "functions-invoke")]
821 #[command(subcommand)]
822 Functions(FunctionsCommands),
823}
824
825/// `fraiseql functions` subcommands.
826#[cfg(feature = "functions-invoke")]
827#[derive(Subcommand)]
828pub(crate) enum FunctionsCommands {
829 /// Run a compiled function in a real V8 isolate against a fixture payload,
830 /// with mocked host ops — the author's inner test loop.
831 ///
832 /// Exit codes: 0 = ran; 3 = the `when` predicate did not match (nothing would
833 /// fire); 4 = the guest errored; 1 = a config/harness error.
834 #[command(long_about = "\
835Run a compiled function against a fixture payload in a real V8 isolate.
836
837The module is loaded exactly as the server loads it (from the compiled schema's
838module_dir); host ops are mocked so no live server, database, or network is needed.
839Every host-op call the function makes is printed.
840
841EXAMPLES:
842 fraiseql functions invoke notifyApproved --payload event.json
843 fraiseql functions invoke notifyApproved --payload event.json --explain
844 fraiseql functions invoke syncDeal --payload deal.json \\
845 --mock-http http.json --mock-query query.json")]
846 Invoke {
847 /// The function name (matches a definition in the compiled schema).
848 name: String,
849
850 /// Path to the fixture payload JSON. For after:mutation / after:capture:
851 /// `{ \"event_kind\": \"update\", \"old\": {…}, \"new\": {…} }` (a bare
852 /// object is treated as an insert's `new` image).
853 #[arg(long)]
854 payload: std::path::PathBuf,
855
856 /// Path to schema.compiled.json.
857 #[arg(long, default_value = "schema.compiled.json")]
858 schema: std::path::PathBuf,
859
860 /// Canned HTTP responses (JSON array of `{url_contains, method?, status?, body?}`);
861 /// a request matching none fails loud.
862 #[arg(long)]
863 mock_http: Option<std::path::PathBuf>,
864
865 /// Canned query responses (JSON array of `{query_contains?, response}`) for the
866 /// `fraiseql_query` bridge; a query matching none fails loud.
867 #[arg(long)]
868 mock_query: Option<std::path::PathBuf>,
869
870 /// Inject a fixed idempotency token the guest reads via the host op.
871 #[arg(long)]
872 idempotency_token: Option<String>,
873
874 /// Print why the `when` predicates did or did not match this payload.
875 #[arg(long)]
876 explain: bool,
877 },
878}
879
880/// `fraiseql perf` subcommands.
881#[derive(Subcommand)]
882pub(crate) enum PerfCommands {
883 /// Flag mutations whose latency regressed between a baseline and a recent
884 /// window, per `(object_type, modification_type)`.
885 ///
886 /// Only rows carrying the current `duration_calc_version` are compared, and
887 /// latency is never aggregated across modification types — so a shift in the
888 /// operation mix cannot mask a regression as a false improvement. Exits 0 even
889 /// when regressions are found (it is a report); pass `--fail-on-regression`
890 /// for CI gating.
891 RegressionScan {
892 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
893 #[arg(long, value_name = "DATABASE_URL")]
894 database: Option<String>,
895
896 /// Recent window size, in days.
897 #[arg(long, default_value_t = 7)]
898 recent_days: i64,
899
900 /// Baseline window size (immediately before the recent window), in days.
901 #[arg(long, default_value_t = 7)]
902 baseline_days: i64,
903
904 /// Minimum comparable samples required on each side; thinner groups are
905 /// skipped rather than flagged.
906 #[arg(long, default_value_t = 20)]
907 min_samples: usize,
908
909 /// Minimum p50 increase to flag, in percent.
910 #[arg(long, default_value_t = 25.0)]
911 threshold_pct: f64,
912
913 /// Minimum absolute p50 increase to flag, in milliseconds (the noise floor).
914 #[arg(long, default_value_t = 5.0)]
915 min_delta_ms: f64,
916
917 /// Restrict the scan to a single object_type.
918 #[arg(long, value_name = "TYPE")]
919 object_type: Option<String>,
920
921 /// Exit non-zero when any regression is found (for CI gating).
922 #[arg(long)]
923 fail_on_regression: bool,
924 },
925
926 /// Ad-hoc forensic reads of the change-log.
927 Explore {
928 #[command(subcommand)]
929 command: PerfExploreCommands,
930 },
931}
932
933/// `fraiseql perf explore` subcommands — read-only views over comparable (v2)
934/// change-log rows in a trailing window.
935#[derive(Subcommand)]
936pub(crate) enum PerfExploreCommands {
937 /// The slowest individual mutations, slowest first.
938 Slowest {
939 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
940 #[arg(long, value_name = "DATABASE_URL")]
941 database: Option<String>,
942
943 /// Trailing window to scan, in days.
944 #[arg(long, default_value_t = 7)]
945 days: i32,
946
947 /// Restrict to a single object_type.
948 #[arg(long, value_name = "TYPE")]
949 object_type: Option<String>,
950
951 /// Maximum rows to return.
952 #[arg(long, default_value_t = 20)]
953 limit: usize,
954 },
955
956 /// `duration_ms` completeness (NULL / pre-fix rates), per operation.
957 NullRate {
958 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
959 #[arg(long, value_name = "DATABASE_URL")]
960 database: Option<String>,
961
962 /// Trailing window to scan, in days.
963 #[arg(long, default_value_t = 7)]
964 days: i32,
965
966 /// Restrict to a single object_type.
967 #[arg(long, value_name = "TYPE")]
968 object_type: Option<String>,
969 },
970
971 /// Latency percentiles (p50/p95/p99/max), per operation.
972 Summary {
973 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
974 #[arg(long, value_name = "DATABASE_URL")]
975 database: Option<String>,
976
977 /// Trailing window to scan, in days.
978 #[arg(long, default_value_t = 7)]
979 days: i32,
980
981 /// Restrict to a single object_type.
982 #[arg(long, value_name = "TYPE")]
983 object_type: Option<String>,
984 },
985}
986
987#[derive(Subcommand)]
988pub(crate) enum ValidateCommands {
989 /// Validate that declared fact tables match database schema
990 Facts {
991 /// Schema.json file path
992 #[arg(short, long, value_name = "SCHEMA")]
993 schema: String,
994
995 /// Database connection string
996 // Long-only: `-d` is the global `--debug` short (#650).
997 #[arg(long, value_name = "DATABASE_URL")]
998 database: String,
999 },
1000}
1001
1002#[derive(Subcommand)]
1003pub(crate) enum GenerateClientCommands {
1004 /// Generate a TypeScript client (interfaces + typed query/mutation functions).
1005 Typescript {
1006 /// Path to schema.compiled.json (auto-detected from conventional
1007 /// locations if omitted).
1008 #[arg(long, value_name = "SCHEMA")]
1009 schema: Option<std::path::PathBuf>,
1010
1011 /// Output directory for the generated client.
1012 #[arg(long, value_name = "DIR")]
1013 out: std::path::PathBuf,
1014
1015 /// Overwrite an existing generated client in the output directory.
1016 #[arg(long, default_value_t = false)]
1017 force: bool,
1018 },
1019
1020 /// Generate a Python client (TypedDicts + typed query/mutation functions;
1021 /// requires Python >= 3.12).
1022 Python {
1023 /// Path to schema.compiled.json (auto-detected from conventional
1024 /// locations if omitted).
1025 #[arg(long, value_name = "SCHEMA")]
1026 schema: Option<std::path::PathBuf>,
1027
1028 /// Output directory for the generated client (becomes a Python package).
1029 #[arg(long, value_name = "DIR")]
1030 out: std::path::PathBuf,
1031
1032 /// Overwrite an existing generated client in the output directory.
1033 #[arg(long, default_value_t = false)]
1034 force: bool,
1035 },
1036
1037 /// Generate a Go client (structs + typed query/mutation functions; requires
1038 /// Go >= 1.21 for the Relay `Connection[T]`).
1039 Go {
1040 /// Path to schema.compiled.json (auto-detected from conventional
1041 /// locations if omitted).
1042 #[arg(long, value_name = "SCHEMA")]
1043 schema: Option<std::path::PathBuf>,
1044
1045 /// Output directory for the generated client (becomes package
1046 /// `fraiseqlclient`).
1047 #[arg(long, value_name = "DIR")]
1048 out: std::path::PathBuf,
1049
1050 /// Overwrite an existing generated client in the output directory.
1051 #[arg(long, default_value_t = false)]
1052 force: bool,
1053 },
1054
1055 /// Generate a Rust client (serde types + typed query/mutation functions;
1056 /// depends only on serde and serde_json).
1057 Rust {
1058 /// Path to schema.compiled.json (auto-detected from conventional
1059 /// locations if omitted).
1060 #[arg(long, value_name = "SCHEMA")]
1061 schema: Option<std::path::PathBuf>,
1062
1063 /// Output directory for the generated client (becomes a module tree with
1064 /// its own `mod.rs`).
1065 #[arg(long, value_name = "DIR")]
1066 out: std::path::PathBuf,
1067
1068 /// Overwrite an existing generated client in the output directory.
1069 #[arg(long, default_value_t = false)]
1070 force: bool,
1071 },
1072}
1073
1074#[derive(Subcommand)]
1075pub(crate) enum FederationCommands {
1076 /// Draw the federation the given subgraphs form
1077 Graph {
1078 /// Each subgraph's schema.compiled.json
1079 #[arg(value_name = "SCHEMA", required = true, num_args = 1..)]
1080 schema: Vec<String>,
1081
1082 /// Output format (json, dot, mermaid)
1083 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
1084 format: String,
1085 },
1086
1087 /// Print a subgraph's federation SDL, exactly as `_service { sdl }` serves it
1088 ///
1089 /// Reads the compiled schema; no server, database or credentials are needed.
1090 #[cfg(feature = "federation")]
1091 Sdl {
1092 /// Path to the subgraph's schema.compiled.json
1093 #[arg(value_name = "SCHEMA")]
1094 schema: std::path::PathBuf,
1095
1096 /// Write the SDL to this file instead of stdout
1097 #[arg(short, long, value_name = "FILE")]
1098 output: Option<std::path::PathBuf>,
1099 },
1100
1101 /// Validate a subgraph's federation entities and keys
1102 Check {
1103 /// Path to local schema.compiled.json
1104 #[arg(value_name = "SCHEMA")]
1105 schema: String,
1106
1107 /// Another subgraph's schema.compiled.json to compare with (@key agreement,
1108 /// field sharing)
1109 #[arg(short, long, value_name = "OTHER_SCHEMA")]
1110 against: Option<String>,
1111
1112 /// Output result as JSON
1113 #[arg(long)]
1114 json: bool,
1115 },
1116}
1117
1118#[derive(Subcommand)]
1119pub(crate) enum IntrospectCommands {
1120 /// Introspect database for fact tables (tf_* tables)
1121 Facts {
1122 /// Database connection string
1123 // Long-only: `-d` is the global `--debug` short (#650).
1124 #[arg(long, value_name = "DATABASE_URL")]
1125 database: String,
1126
1127 /// Output format (python, json)
1128 #[arg(short, long, value_name = "FORMAT", default_value = "python")]
1129 format: String,
1130 },
1131}
1132
1133#[derive(Subcommand)]
1134pub(crate) enum SchemaCommands {
1135 /// Display field-level security metadata from a running server
1136 Metadata {
1137 /// Server base URL
1138 #[arg(
1139 short,
1140 long,
1141 value_name = "URL",
1142 default_value = "http://localhost:8080"
1143 )]
1144 server: String,
1145
1146 /// Bearer token for authentication
1147 #[arg(short, long, value_name = "TOKEN")]
1148 token: Option<String>,
1149 },
1150}
1151
1152#[derive(Subcommand)]
1153pub(crate) enum MigrateCommands {
1154 /// Apply pending migrations
1155 Up {
1156 /// Database connection URL
1157 #[arg(long, value_name = "DATABASE_URL")]
1158 database: Option<String>,
1159
1160 /// Migration directory
1161 #[arg(long, value_name = "DIR")]
1162 dir: Option<String>,
1163 },
1164
1165 /// Roll back migrations
1166 Down {
1167 /// Database connection URL
1168 #[arg(long, value_name = "DATABASE_URL")]
1169 database: Option<String>,
1170
1171 /// Migration directory
1172 #[arg(long, value_name = "DIR")]
1173 dir: Option<String>,
1174
1175 /// Number of migrations to roll back
1176 #[arg(long, default_value = "1")]
1177 steps: u32,
1178 },
1179
1180 /// Show migration status
1181 Status {
1182 /// Database connection URL
1183 #[arg(long, value_name = "DATABASE_URL")]
1184 database: Option<String>,
1185
1186 /// Migration directory
1187 #[arg(long, value_name = "DIR")]
1188 dir: Option<String>,
1189 },
1190
1191 /// Create a new migration file
1192 ///
1193 /// Delegates to `confiture migrate generate`, like `generate`; kept as the
1194 /// verb the examples above use.
1195 Create {
1196 /// Migration name
1197 #[arg(value_name = "NAME")]
1198 name: String,
1199
1200 /// Migration directory
1201 #[arg(long, value_name = "DIR")]
1202 dir: Option<String>,
1203 },
1204
1205 /// Generate a new migration from schema diff
1206 ///
1207 /// Delegates to `confiture migrate generate`.
1208 /// Creates a timestamped migration file.
1209 Generate {
1210 /// Migration name
1211 #[arg(value_name = "NAME")]
1212 name: String,
1213
1214 /// Migration directory
1215 #[arg(long, value_name = "DIR")]
1216 dir: Option<String>,
1217 },
1218
1219 /// Validate migration files for naming, idempotency, and drift
1220 ///
1221 /// Delegates to `confiture migrate validate`.
1222 /// Checks naming conventions, idempotency, and schema drift.
1223 Validate {
1224 /// Migration directory
1225 #[arg(long, value_name = "DIR")]
1226 dir: Option<String>,
1227 },
1228
1229 /// Pre-deploy safety check on pending migrations
1230 ///
1231 /// Delegates to `confiture migrate preflight`.
1232 /// Verifies reversibility, detects non-transactional statements, checks checksums.
1233 Preflight {
1234 /// Migration directory
1235 #[arg(long, value_name = "DIR")]
1236 dir: Option<String>,
1237 },
1238}