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