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
118 /// Extract schema from annotated source files
119 ///
120 /// Parses FraiseQL annotations in any supported language and generates schema.json.
121 /// No language runtime required — pure text processing.
122 #[command(after_help = "\
123EXAMPLES:
124 fraiseql extract schema/schema.py
125 fraiseql extract schema/ --recursive
126 fraiseql extract schema.rs --language rust -o schema.json")]
127 Extract {
128 /// Source file(s) or directory to extract from
129 #[arg(value_name = "INPUT")]
130 input: Vec<String>,
131
132 /// Override language detection (python, typescript, rust, java, kotlin, go, csharp, swift,
133 /// scala)
134 #[arg(short, long)]
135 language: Option<String>,
136
137 /// Recursively scan directories
138 #[arg(short, long)]
139 recursive: bool,
140
141 /// Output file path
142 #[arg(short, long, default_value = "schema.json")]
143 output: String,
144 },
145
146 /// Explain query execution plan and complexity
147 ///
148 /// Shows GraphQL query execution plan, SQL, and complexity analysis.
149 #[command(after_help = "\
150EXAMPLES:
151 fraiseql explain '{ users { id name } }'
152 fraiseql explain '{ user(id: 1) { posts { title } } }' --json")]
153 Explain {
154 /// GraphQL query string
155 #[arg(value_name = "QUERY")]
156 query: String,
157 },
158
159 /// Calculate query complexity score
160 ///
161 /// Quick analysis of query complexity (depth, field count, score).
162 #[command(after_help = "\
163EXAMPLES:
164 fraiseql cost '{ users { id name } }'
165 fraiseql cost '{ deeply { nested { query { here } } } }' --json")]
166 Cost {
167 /// GraphQL query string
168 #[arg(value_name = "QUERY")]
169 query: String,
170 },
171
172 /// Analyze schema for optimization opportunities
173 ///
174 /// Provides recommendations across 6 categories:
175 /// performance, security, federation, complexity, caching, indexing
176 #[command(after_help = "\
177EXAMPLES:
178 fraiseql analyze schema.compiled.json
179 fraiseql analyze schema.compiled.json --json")]
180 Analyze {
181 /// Path to schema.compiled.json
182 #[arg(value_name = "SCHEMA")]
183 schema: String,
184 },
185
186 /// Analyze schema type dependencies
187 ///
188 /// Exports dependency graph, detects cycles, and finds unused types.
189 /// Supports multiple output formats for visualization and CI integration.
190 #[command(after_help = "\
191EXAMPLES:
192 fraiseql dependency-graph schema.compiled.json
193 fraiseql dependency-graph schema.compiled.json -f dot > graph.dot
194 fraiseql dependency-graph schema.compiled.json -f mermaid
195 fraiseql dependency-graph schema.compiled.json --json")]
196 DependencyGraph {
197 /// Path to schema.compiled.json
198 #[arg(value_name = "SCHEMA")]
199 schema: String,
200
201 /// Output format (json, dot, mermaid, d2, console)
202 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
203 format: String,
204 },
205
206 /// Export federation dependency graph
207 ///
208 /// Visualize federation structure in multiple formats.
209 #[command(after_help = "\
210EXAMPLES:
211 fraiseql federation graph schema.compiled.json
212 fraiseql federation graph schema.compiled.json -f dot
213 fraiseql federation graph schema.compiled.json -f mermaid")]
214 Federation {
215 /// Schema path (positional argument passed to subcommand)
216 #[command(subcommand)]
217 command: FederationCommands,
218 },
219
220 /// Lint schema for FraiseQL design quality
221 ///
222 /// Analyzes schema using FraiseQL-calibrated design rules.
223 /// Detects JSONB batching issues, compilation problems, auth boundaries, etc.
224 #[command(after_help = "\
225EXAMPLES:
226 fraiseql lint schema.json
227 fraiseql lint schema.compiled.json --federation
228 fraiseql lint schema.json --fail-on-critical
229 fraiseql lint schema.json --json")]
230 Lint {
231 /// Path to schema.json or schema.compiled.json
232 #[arg(value_name = "SCHEMA")]
233 schema: String,
234
235 /// Only show federation audit
236 #[arg(long)]
237 federation: bool,
238
239 /// Only show cost audit
240 #[arg(long)]
241 cost: bool,
242
243 /// Only show cache audit
244 #[arg(long)]
245 cache: bool,
246
247 /// Only show auth audit
248 #[arg(long)]
249 auth: bool,
250
251 /// Only show compilation audit
252 #[arg(long)]
253 compilation: bool,
254
255 /// Exit with error if any critical issues found
256 #[arg(long)]
257 fail_on_critical: bool,
258
259 /// Exit with error if any warning or critical issues found
260 #[arg(long)]
261 fail_on_warning: bool,
262
263 /// Show detailed issue descriptions
264 #[arg(long)]
265 verbose: bool,
266 },
267
268 /// Generate DDL for Arrow views (va_*, tv_*, ta_*)
269 #[command(after_help = "\
270EXAMPLES:
271 fraiseql generate-views -s schema.json -e User --view va_users
272 fraiseql generate-views -s schema.json -e Order --view tv_orders --refresh-strategy scheduled")]
273 GenerateViews {
274 /// Path to schema.json
275 #[arg(short, long, value_name = "SCHEMA")]
276 schema: String,
277
278 /// Entity name from schema
279 #[arg(short, long, value_name = "NAME")]
280 entity: String,
281
282 /// View name (must start with va_, tv_, or ta_)
283 #[arg(long, value_name = "NAME")]
284 view: String,
285
286 /// Refresh strategy (trigger-based or scheduled)
287 #[arg(long, value_name = "STRATEGY", default_value = "trigger-based")]
288 refresh_strategy: String,
289
290 /// Output file path (default: {view}.sql)
291 #[arg(short, long, value_name = "PATH")]
292 output: Option<String>,
293
294 /// Include helper/composition views
295 #[arg(long, default_value = "true")]
296 include_composition_views: bool,
297
298 /// Include monitoring functions
299 #[arg(long, default_value = "true")]
300 include_monitoring: bool,
301
302 /// Validate only, don't write file
303 #[arg(long)]
304 validate: bool,
305
306 /// Show generation steps (use global --verbose flag)
307 #[arg(long, action = clap::ArgAction::SetTrue)]
308 gen_verbose: bool,
309 },
310
311 /// Generate external-write capture-trigger DDL for @subscribable types (#366)
312 ///
313 /// Emits a self-contained, idempotent PostgreSQL script (the capture function
314 /// plus per-table statement-level triggers) that brings raw external writes
315 /// (psql / migrations / third-party tools) to a `@subscribable` type's table(s)
316 /// onto the Change Spine, without double-emitting for FraiseQL-mediated writes.
317 #[command(after_help = "\
318EXAMPLES:
319 fraiseql generate-capture-triggers -s schema.compiled.json | psql \"$DATABASE_URL\"
320 fraiseql generate-capture-triggers -s schema.compiled.json -o capture.sql")]
321 GenerateCaptureTriggers {
322 /// Path to the compiled schema (schema.compiled.json)
323 #[arg(short, long, value_name = "SCHEMA")]
324 schema: String,
325
326 /// Output file path (default: stdout, for piping into psql)
327 #[arg(short, long, value_name = "PATH")]
328 output: Option<String>,
329
330 /// Omit the capture function definition (emit only the per-table triggers)
331 #[arg(long)]
332 no_function: bool,
333 },
334
335 /// Validate schema.json or fact tables
336 ///
337 /// Performs comprehensive schema validation including:
338 /// - JSON structure validation
339 /// - Type reference validation
340 /// - Circular dependency detection (with --check-cycles)
341 /// - Unused type detection (with --check-unused)
342 #[command(after_help = "\
343EXAMPLES:
344 fraiseql validate schema.json
345 fraiseql validate schema.json --check-unused
346 fraiseql validate schema.json --strict
347 fraiseql validate facts -s schema.json -d postgres://localhost/db")]
348 Validate {
349 #[command(subcommand)]
350 command: Option<ValidateCommands>,
351
352 /// Schema.json file path to validate (if no subcommand)
353 #[arg(value_name = "INPUT")]
354 input: Option<String>,
355
356 /// Check for circular dependencies between types
357 #[arg(long, default_value = "true")]
358 check_cycles: bool,
359
360 /// Check for unused types (no incoming references)
361 #[arg(long)]
362 check_unused: bool,
363
364 /// Strict mode: treat warnings as errors (unused types become errors)
365 #[arg(long)]
366 strict: bool,
367
368 /// Only analyze specific type(s) - comma-separated list
369 #[arg(long, value_name = "TYPES", value_delimiter = ',')]
370 types: Vec<String>,
371
372 /// Validate the mutation contract against a live PostgreSQL database.
373 ///
374 /// For each DB-backed mutation, checks that its `sql_source` function
375 /// exists with the argument layout the server sends and returns the
376 /// columns the server decodes into `MutationResponse`. Requires a
377 /// compiled schema (schema.compiled.json) as INPUT. (#397)
378 #[arg(long, value_name = "DATABASE_URL")]
379 against_db: Option<String>,
380 },
381
382 /// Introspect database for fact tables and output suggestions
383 #[command(after_help = "\
384EXAMPLES:
385 fraiseql introspect facts -d postgres://localhost/db
386 fraiseql introspect facts -d postgres://localhost/db -f json")]
387 Introspect {
388 #[command(subcommand)]
389 command: IntrospectCommands,
390 },
391
392 /// Generate authoring-language source from schema.json
393 ///
394 /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
395 /// source code in any of the 9 supported authoring languages.
396 #[command(after_help = "\
397EXAMPLES:
398 fraiseql generate schema.json --language python
399 fraiseql generate schema.json --language rust -o schema.rs
400 fraiseql generate schema.json --language typescript")]
401 Generate {
402 /// Path to schema.json
403 #[arg(value_name = "INPUT")]
404 input: String,
405
406 /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
407 #[arg(short, long)]
408 language: String,
409
410 /// Output file path (default: schema.<ext> based on language)
411 #[arg(short, long)]
412 output: Option<String>,
413 },
414
415 /// Generate consumer-side typed clients from a compiled schema
416 ///
417 /// Distinct from `generate`: `generate` emits server-side authoring code (write
418 /// FraiseQL types in another language, fed back into the compiler), whereas
419 /// `generate-client` emits a typed client for callers of a FraiseQL API
420 /// (interfaces + query/mutation functions) from a compiled schema.compiled.json.
421 #[command(after_help = "\
422EXAMPLES:
423 fraiseql generate-client typescript --out ./src/generated
424 fraiseql generate-client typescript --schema ./schema.compiled.json --out ./gen --force")]
425 GenerateClient {
426 #[command(subcommand)]
427 language: GenerateClientCommands,
428 },
429
430 /// Initialize a new FraiseQL project
431 ///
432 /// Creates project directory with fraiseql.toml, schema.json,
433 /// database DDL structure, and authoring skeleton.
434 #[command(after_help = "\
435EXAMPLES:
436 fraiseql init my-app
437 fraiseql init my-app --language typescript --database postgres
438 fraiseql init my-app --size xs --no-git")]
439 Init {
440 /// Project name (used as directory name)
441 #[arg(value_name = "PROJECT_NAME")]
442 project_name: String,
443
444 /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
445 #[arg(short, long, default_value = "python")]
446 language: String,
447
448 /// Target database (postgres, mysql, sqlite, sqlserver)
449 #[arg(long, default_value = "postgres")]
450 database: String,
451
452 /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
453 #[arg(long, default_value = "s")]
454 size: String,
455
456 /// Skip git init
457 #[arg(long)]
458 no_git: bool,
459 },
460
461 /// Run database migrations
462 ///
463 /// Wraps confiture for a unified migration experience.
464 /// Reads database URL from --database, fraiseql.toml, or DATABASE_URL env var.
465 #[command(after_help = "\
466EXAMPLES:
467 fraiseql migrate up --database postgres://localhost/mydb
468 fraiseql migrate down --steps 1
469 fraiseql migrate status
470 fraiseql migrate create add_posts_table")]
471 Migrate {
472 #[command(subcommand)]
473 command: MigrateCommands,
474 },
475
476 /// Generate Software Bill of Materials
477 ///
478 /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
479 #[command(after_help = "\
480EXAMPLES:
481 fraiseql sbom
482 fraiseql sbom --format spdx
483 fraiseql sbom --format cyclonedx --output sbom.json")]
484 Sbom {
485 /// Output format (cyclonedx, spdx)
486 #[arg(short, long, default_value = "cyclonedx")]
487 format: String,
488
489 /// Output file path (default: stdout)
490 #[arg(short, long, value_name = "FILE")]
491 output: Option<String>,
492 },
493
494 /// Compile schema and immediately start the GraphQL server
495 ///
496 /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
497 /// With --watch, the server hot-reloads whenever the schema file changes.
498 ///
499 /// Server and database settings can be declared in fraiseql.toml under [server]
500 /// and [database] sections. CLI flags take precedence over TOML settings, which
501 /// take precedence over defaults. The database URL is resolved in this order:
502 /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
503 #[cfg(feature = "run-server")]
504 #[command(after_help = "\
505EXAMPLES:
506 fraiseql run
507 fraiseql run fraiseql.toml --database postgres://localhost/mydb
508 fraiseql run --port 3000 --watch
509 fraiseql run schema.json --introspection
510
511TOML CONFIG:
512 [server]
513 host = \"127.0.0.1\"
514 port = 9000
515
516 [server.cors]
517 origins = [\"https://app.example.com\"]
518
519 [database]
520 url = \"${DATABASE_URL}\"
521 pool_min = 2
522 pool_max = 20")]
523 Run {
524 /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
525 #[arg(value_name = "INPUT")]
526 input: Option<String>,
527
528 /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
529 #[arg(short, long, value_name = "DATABASE_URL")]
530 database: Option<String>,
531
532 /// Port to listen on (overrides [server].port in fraiseql.toml)
533 #[arg(short, long, value_name = "PORT")]
534 port: Option<u16>,
535
536 /// Bind address (overrides [server].host in fraiseql.toml)
537 #[arg(long, value_name = "HOST")]
538 bind: Option<String>,
539
540 /// Watch input file for changes and hot-reload the server
541 #[arg(short, long)]
542 watch: bool,
543
544 /// Enable the GraphQL introspection endpoint (no auth required)
545 #[arg(long)]
546 introspection: bool,
547 },
548
549 /// Validate a trusted documents manifest
550 ///
551 /// Checks that the manifest JSON is well-formed and that each key
552 /// is a valid SHA-256 hex string matching its query body.
553 #[command(after_help = "\
554EXAMPLES:
555 fraiseql validate-documents manifest.json")]
556 ValidateDocuments {
557 /// Path to the trusted documents manifest JSON file
558 #[arg(value_name = "MANIFEST")]
559 manifest: String,
560 },
561
562 /// Development server with hot-reload
563 #[command(hide = true)] // Hide until implemented
564 Serve {
565 /// Schema.json file path to watch
566 #[arg(value_name = "SCHEMA")]
567 schema: String,
568
569 /// Port to listen on
570 #[arg(short, long, default_value = "8080")]
571 port: u16,
572 },
573
574 /// Install FraiseQL mutation helper functions
575 ///
576 /// Installs SQL helper functions (fraiseql.mutation_ok, fraiseql.mutation_err, etc.)
577 /// to reduce boilerplate when writing mutation functions under the v2.2.0 protocol.
578 /// The helpers are installed in the `fraiseql` schema, which is owned by FraiseQL's
579 /// database role.
580 #[command(after_help = "\
581EXAMPLES:
582 fraiseql setup --database postgres://localhost/mydb
583 fraiseql setup --dry-run
584 fraiseql setup # Uses DATABASE_URL or [database].url from fraiseql.toml")]
585 Setup {
586 /// Database connection URL (or use DATABASE_URL env var, or [database].url in
587 /// fraiseql.toml)
588 #[arg(long, value_name = "DATABASE_URL")]
589 database: Option<String>,
590
591 /// Print SQL without applying changes
592 #[arg(long)]
593 dry_run: bool,
594 },
595
596 /// Inspect schema metadata from a running FraiseQL server
597 ///
598 /// Fetches field-level security metadata (encryption, scope requirements, deny actions)
599 /// from the server's `/api/v1/schema/metadata` endpoint and displays it as a table.
600 #[command(after_help = "\
601EXAMPLES:
602 fraiseql schema metadata
603 fraiseql schema metadata --server http://localhost:8080
604 fraiseql schema metadata --server https://api.example.com --token mytoken")]
605 Schema {
606 #[command(subcommand)]
607 command: SchemaCommands,
608 },
609
610 /// Run diagnostic checks for common FraiseQL setup problems
611 ///
612 /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
613 /// and cache/auth coherence. Prints a color-coded report and exits 0 if
614 /// all checks pass or 1 if any check fails (warnings do not fail).
615 #[command(after_help = "\
616EXAMPLES:
617 fraiseql doctor
618 fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
619 fraiseql doctor --db-url postgres://user:pass@host:5432/db
620 fraiseql doctor --json")]
621 Doctor {
622 /// Path to fraiseql.toml configuration file.
623 #[arg(long, default_value = "fraiseql.toml")]
624 config: std::path::PathBuf,
625
626 /// Path to schema.compiled.json.
627 #[arg(long, default_value = "schema.compiled.json")]
628 schema: std::path::PathBuf,
629
630 /// Override DATABASE_URL for the connectivity check.
631 #[arg(long)]
632 db_url: Option<String>,
633
634 /// Run the live-database passes against this PostgreSQL database: the
635 /// change-log contract drift check (compares core.tb_entity_change_log
636 /// against the shipped contract — #380) and the PL/pgSQL
637 /// body-resolution pass (reports internal calls that no longer resolve
638 /// — #409).
639 ///
640 /// The body-resolution pass requires the `plpgsql_check` extension and
641 /// skips with a hint when it is unavailable.
642 #[arg(long, value_name = "DATABASE_URL")]
643 against_db: Option<String>,
644
645 /// Schemas to scan in the body-resolution pass (comma-separated).
646 #[arg(
647 long,
648 value_name = "SCHEMAS",
649 value_delimiter = ',',
650 default_value = "public"
651 )]
652 schemas: Vec<String>,
653
654 /// Output machine-readable JSON (for CI integration).
655 #[arg(long)]
656 json: bool,
657 },
658
659 /// Change-log performance observability (#392)
660 ///
661 /// Reads the framework-owned change-log (`core.v_entity_change_log`) and turns
662 /// it into operator-facing forensics: latency regressions between two time
663 /// windows, and ad-hoc reads of the slowest mutations / duration completeness
664 /// / per-operation percentiles.
665 Perf {
666 #[command(subcommand)]
667 command: PerfCommands,
668 },
669}
670
671/// `fraiseql perf` subcommands.
672#[derive(Subcommand)]
673pub(crate) enum PerfCommands {
674 /// Flag mutations whose latency regressed between a baseline and a recent
675 /// window, per `(object_type, modification_type)`.
676 ///
677 /// Only rows carrying the current `duration_calc_version` are compared, and
678 /// latency is never aggregated across modification types — so a shift in the
679 /// operation mix cannot mask a regression as a false improvement. Exits 0 even
680 /// when regressions are found (it is a report); pass `--fail-on-regression`
681 /// for CI gating.
682 RegressionScan {
683 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
684 #[arg(long, value_name = "DATABASE_URL")]
685 database: Option<String>,
686
687 /// Recent window size, in days.
688 #[arg(long, default_value_t = 7)]
689 recent_days: i64,
690
691 /// Baseline window size (immediately before the recent window), in days.
692 #[arg(long, default_value_t = 7)]
693 baseline_days: i64,
694
695 /// Minimum comparable samples required on each side; thinner groups are
696 /// skipped rather than flagged.
697 #[arg(long, default_value_t = 20)]
698 min_samples: usize,
699
700 /// Minimum p50 increase to flag, in percent.
701 #[arg(long, default_value_t = 25.0)]
702 threshold_pct: f64,
703
704 /// Minimum absolute p50 increase to flag, in milliseconds (the noise floor).
705 #[arg(long, default_value_t = 5.0)]
706 min_delta_ms: f64,
707
708 /// Restrict the scan to a single object_type.
709 #[arg(long, value_name = "TYPE")]
710 object_type: Option<String>,
711
712 /// Exit non-zero when any regression is found (for CI gating).
713 #[arg(long)]
714 fail_on_regression: bool,
715 },
716
717 /// Ad-hoc forensic reads of the change-log.
718 Explore {
719 #[command(subcommand)]
720 command: PerfExploreCommands,
721 },
722}
723
724/// `fraiseql perf explore` subcommands — read-only views over comparable (v2)
725/// change-log rows in a trailing window.
726#[derive(Subcommand)]
727pub(crate) enum PerfExploreCommands {
728 /// The slowest individual mutations, slowest first.
729 Slowest {
730 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
731 #[arg(long, value_name = "DATABASE_URL")]
732 database: Option<String>,
733
734 /// Trailing window to scan, in days.
735 #[arg(long, default_value_t = 7)]
736 days: i32,
737
738 /// Restrict to a single object_type.
739 #[arg(long, value_name = "TYPE")]
740 object_type: Option<String>,
741
742 /// Maximum rows to return.
743 #[arg(long, default_value_t = 20)]
744 limit: usize,
745 },
746
747 /// `duration_ms` completeness (NULL / pre-fix rates), per operation.
748 NullRate {
749 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
750 #[arg(long, value_name = "DATABASE_URL")]
751 database: Option<String>,
752
753 /// Trailing window to scan, in days.
754 #[arg(long, default_value_t = 7)]
755 days: i32,
756
757 /// Restrict to a single object_type.
758 #[arg(long, value_name = "TYPE")]
759 object_type: Option<String>,
760 },
761
762 /// Latency percentiles (p50/p95/p99/max), per operation.
763 Summary {
764 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
765 #[arg(long, value_name = "DATABASE_URL")]
766 database: Option<String>,
767
768 /// Trailing window to scan, in days.
769 #[arg(long, default_value_t = 7)]
770 days: i32,
771
772 /// Restrict to a single object_type.
773 #[arg(long, value_name = "TYPE")]
774 object_type: Option<String>,
775 },
776}
777
778#[derive(Subcommand)]
779pub(crate) enum ValidateCommands {
780 /// Validate that declared fact tables match database schema
781 Facts {
782 /// Schema.json file path
783 #[arg(short, long, value_name = "SCHEMA")]
784 schema: String,
785
786 /// Database connection string
787 #[arg(short, long, value_name = "DATABASE_URL")]
788 database: String,
789 },
790}
791
792#[derive(Subcommand)]
793pub(crate) enum GenerateClientCommands {
794 /// Generate a TypeScript client (interfaces + typed query/mutation functions).
795 Typescript {
796 /// Path to schema.compiled.json (auto-detected from conventional
797 /// locations if omitted).
798 #[arg(long, value_name = "SCHEMA")]
799 schema: Option<std::path::PathBuf>,
800
801 /// Output directory for the generated client.
802 #[arg(long, value_name = "DIR")]
803 out: std::path::PathBuf,
804
805 /// Overwrite an existing generated client in the output directory.
806 #[arg(long, default_value_t = false)]
807 force: bool,
808 },
809}
810
811#[derive(Subcommand)]
812pub(crate) enum FederationCommands {
813 /// Export federation graph
814 Graph {
815 /// Path to schema.compiled.json
816 #[arg(value_name = "SCHEMA")]
817 schema: String,
818
819 /// Output format (json, dot, mermaid)
820 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
821 format: String,
822 },
823
824 /// Validate subgraph composition
825 Check {
826 /// Path to local schema.compiled.json
827 #[arg(value_name = "SCHEMA")]
828 schema: String,
829
830 /// Path to supergraph schema for composition validation
831 #[arg(short, long, value_name = "SUPERGRAPH")]
832 against: Option<String>,
833
834 /// Output result as JSON
835 #[arg(long)]
836 json: bool,
837 },
838}
839
840#[derive(Subcommand)]
841pub(crate) enum IntrospectCommands {
842 /// Introspect database for fact tables (tf_* tables)
843 Facts {
844 /// Database connection string
845 #[arg(short, long, value_name = "DATABASE_URL")]
846 database: String,
847
848 /// Output format (python, json)
849 #[arg(short, long, value_name = "FORMAT", default_value = "python")]
850 format: String,
851 },
852}
853
854#[derive(Subcommand)]
855pub(crate) enum SchemaCommands {
856 /// Display field-level security metadata from a running server
857 Metadata {
858 /// Server base URL
859 #[arg(
860 short,
861 long,
862 value_name = "URL",
863 default_value = "http://localhost:8080"
864 )]
865 server: String,
866
867 /// Bearer token for authentication
868 #[arg(short, long, value_name = "TOKEN")]
869 token: Option<String>,
870 },
871}
872
873#[derive(Subcommand)]
874pub(crate) enum MigrateCommands {
875 /// Apply pending migrations
876 Up {
877 /// Database connection URL
878 #[arg(long, value_name = "DATABASE_URL")]
879 database: Option<String>,
880
881 /// Migration directory
882 #[arg(long, value_name = "DIR")]
883 dir: Option<String>,
884 },
885
886 /// Roll back migrations
887 Down {
888 /// Database connection URL
889 #[arg(long, value_name = "DATABASE_URL")]
890 database: Option<String>,
891
892 /// Migration directory
893 #[arg(long, value_name = "DIR")]
894 dir: Option<String>,
895
896 /// Number of migrations to roll back
897 #[arg(long, default_value = "1")]
898 steps: u32,
899 },
900
901 /// Show migration status
902 Status {
903 /// Database connection URL
904 #[arg(long, value_name = "DATABASE_URL")]
905 database: Option<String>,
906
907 /// Migration directory
908 #[arg(long, value_name = "DIR")]
909 dir: Option<String>,
910 },
911
912 /// Create a new migration file
913 Create {
914 /// Migration name
915 #[arg(value_name = "NAME")]
916 name: String,
917
918 /// Migration directory
919 #[arg(long, value_name = "DIR")]
920 dir: Option<String>,
921 },
922
923 /// Generate a new migration from schema diff
924 ///
925 /// Delegates to `confiture migrate generate`.
926 /// Creates a timestamped migration file.
927 Generate {
928 /// Migration name
929 #[arg(value_name = "NAME")]
930 name: String,
931
932 /// Migration directory
933 #[arg(long, value_name = "DIR")]
934 dir: Option<String>,
935 },
936
937 /// Validate migration files for naming, idempotency, and drift
938 ///
939 /// Delegates to `confiture migrate validate`.
940 /// Checks naming conventions, idempotency, and schema drift.
941 Validate {
942 /// Migration directory
943 #[arg(long, value_name = "DIR")]
944 dir: Option<String>,
945 },
946
947 /// Pre-deploy safety check on pending migrations
948 ///
949 /// Delegates to `confiture migrate preflight`.
950 /// Verifies reversibility, detects non-transactional statements, checks checksums.
951 Preflight {
952 /// Migration directory
953 #[arg(long, value_name = "DIR")]
954 dir: Option<String>,
955 },
956}