Skip to main content

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    /// Validate schema.json or fact tables
312    ///
313    /// Performs comprehensive schema validation including:
314    /// - JSON structure validation
315    /// - Type reference validation
316    /// - Circular dependency detection (with --check-cycles)
317    /// - Unused type detection (with --check-unused)
318    #[command(after_help = "\
319EXAMPLES:
320    fraiseql validate schema.json
321    fraiseql validate schema.json --check-unused
322    fraiseql validate schema.json --strict
323    fraiseql validate facts -s schema.json -d postgres://localhost/db")]
324    Validate {
325        #[command(subcommand)]
326        command: Option<ValidateCommands>,
327
328        /// Schema.json file path to validate (if no subcommand)
329        #[arg(value_name = "INPUT")]
330        input: Option<String>,
331
332        /// Check for circular dependencies between types
333        #[arg(long, default_value = "true")]
334        check_cycles: bool,
335
336        /// Check for unused types (no incoming references)
337        #[arg(long)]
338        check_unused: bool,
339
340        /// Strict mode: treat warnings as errors (unused types become errors)
341        #[arg(long)]
342        strict: bool,
343
344        /// Only analyze specific type(s) - comma-separated list
345        #[arg(long, value_name = "TYPES", value_delimiter = ',')]
346        types: Vec<String>,
347
348        /// Validate the mutation contract against a live PostgreSQL database.
349        ///
350        /// For each DB-backed mutation, checks that its `sql_source` function
351        /// exists with the argument layout the server sends and returns the
352        /// columns the server decodes into `MutationResponse`. Requires a
353        /// compiled schema (schema.compiled.json) as INPUT. (#397)
354        #[arg(long, value_name = "DATABASE_URL")]
355        against_db: Option<String>,
356    },
357
358    /// Introspect database for fact tables and output suggestions
359    #[command(after_help = "\
360EXAMPLES:
361    fraiseql introspect facts -d postgres://localhost/db
362    fraiseql introspect facts -d postgres://localhost/db -f json")]
363    Introspect {
364        #[command(subcommand)]
365        command: IntrospectCommands,
366    },
367
368    /// Generate authoring-language source from schema.json
369    ///
370    /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
371    /// source code in any of the 9 supported authoring languages.
372    #[command(after_help = "\
373EXAMPLES:
374    fraiseql generate schema.json --language python
375    fraiseql generate schema.json --language rust -o schema.rs
376    fraiseql generate schema.json --language typescript")]
377    Generate {
378        /// Path to schema.json
379        #[arg(value_name = "INPUT")]
380        input: String,
381
382        /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
383        #[arg(short, long)]
384        language: String,
385
386        /// Output file path (default: schema.<ext> based on language)
387        #[arg(short, long)]
388        output: Option<String>,
389    },
390
391    /// Generate consumer-side typed clients from a compiled schema
392    ///
393    /// Distinct from `generate`: `generate` emits server-side authoring code (write
394    /// FraiseQL types in another language, fed back into the compiler), whereas
395    /// `generate-client` emits a typed client for callers of a FraiseQL API
396    /// (interfaces + query/mutation functions) from a compiled schema.compiled.json.
397    #[command(after_help = "\
398EXAMPLES:
399    fraiseql generate-client typescript --out ./src/generated
400    fraiseql generate-client typescript --schema ./schema.compiled.json --out ./gen --force")]
401    GenerateClient {
402        #[command(subcommand)]
403        language: GenerateClientCommands,
404    },
405
406    /// Initialize a new FraiseQL project
407    ///
408    /// Creates project directory with fraiseql.toml, schema.json,
409    /// database DDL structure, and authoring skeleton.
410    #[command(after_help = "\
411EXAMPLES:
412    fraiseql init my-app
413    fraiseql init my-app --language typescript --database postgres
414    fraiseql init my-app --size xs --no-git")]
415    Init {
416        /// Project name (used as directory name)
417        #[arg(value_name = "PROJECT_NAME")]
418        project_name: String,
419
420        /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
421        #[arg(short, long, default_value = "python")]
422        language: String,
423
424        /// Target database (postgres, mysql, sqlite, sqlserver)
425        #[arg(long, default_value = "postgres")]
426        database: String,
427
428        /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
429        #[arg(long, default_value = "s")]
430        size: String,
431
432        /// Skip git init
433        #[arg(long)]
434        no_git: bool,
435    },
436
437    /// Run database migrations
438    ///
439    /// Wraps confiture for a unified migration experience.
440    /// Reads database URL from --database, fraiseql.toml, or DATABASE_URL env var.
441    #[command(after_help = "\
442EXAMPLES:
443    fraiseql migrate up --database postgres://localhost/mydb
444    fraiseql migrate down --steps 1
445    fraiseql migrate status
446    fraiseql migrate create add_posts_table")]
447    Migrate {
448        #[command(subcommand)]
449        command: MigrateCommands,
450    },
451
452    /// Generate Software Bill of Materials
453    ///
454    /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
455    #[command(after_help = "\
456EXAMPLES:
457    fraiseql sbom
458    fraiseql sbom --format spdx
459    fraiseql sbom --format cyclonedx --output sbom.json")]
460    Sbom {
461        /// Output format (cyclonedx, spdx)
462        #[arg(short, long, default_value = "cyclonedx")]
463        format: String,
464
465        /// Output file path (default: stdout)
466        #[arg(short, long, value_name = "FILE")]
467        output: Option<String>,
468    },
469
470    /// Compile schema and immediately start the GraphQL server
471    ///
472    /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
473    /// With --watch, the server hot-reloads whenever the schema file changes.
474    ///
475    /// Server and database settings can be declared in fraiseql.toml under [server]
476    /// and [database] sections.  CLI flags take precedence over TOML settings, which
477    /// take precedence over defaults.  The database URL is resolved in this order:
478    /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
479    #[cfg(feature = "run-server")]
480    #[command(after_help = "\
481EXAMPLES:
482    fraiseql run
483    fraiseql run fraiseql.toml --database postgres://localhost/mydb
484    fraiseql run --port 3000 --watch
485    fraiseql run schema.json --introspection
486
487TOML CONFIG:
488    [server]
489    host = \"127.0.0.1\"
490    port = 9000
491
492    [server.cors]
493    origins = [\"https://app.example.com\"]
494
495    [database]
496    url      = \"${DATABASE_URL}\"
497    pool_min = 2
498    pool_max = 20")]
499    Run {
500        /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
501        #[arg(value_name = "INPUT")]
502        input: Option<String>,
503
504        /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
505        #[arg(short, long, value_name = "DATABASE_URL")]
506        database: Option<String>,
507
508        /// Port to listen on (overrides [server].port in fraiseql.toml)
509        #[arg(short, long, value_name = "PORT")]
510        port: Option<u16>,
511
512        /// Bind address (overrides [server].host in fraiseql.toml)
513        #[arg(long, value_name = "HOST")]
514        bind: Option<String>,
515
516        /// Watch input file for changes and hot-reload the server
517        #[arg(short, long)]
518        watch: bool,
519
520        /// Enable the GraphQL introspection endpoint (no auth required)
521        #[arg(long)]
522        introspection: bool,
523    },
524
525    /// Validate a trusted documents manifest
526    ///
527    /// Checks that the manifest JSON is well-formed and that each key
528    /// is a valid SHA-256 hex string matching its query body.
529    #[command(after_help = "\
530EXAMPLES:
531    fraiseql validate-documents manifest.json")]
532    ValidateDocuments {
533        /// Path to the trusted documents manifest JSON file
534        #[arg(value_name = "MANIFEST")]
535        manifest: String,
536    },
537
538    /// Development server with hot-reload
539    #[command(hide = true)] // Hide until implemented
540    Serve {
541        /// Schema.json file path to watch
542        #[arg(value_name = "SCHEMA")]
543        schema: String,
544
545        /// Port to listen on
546        #[arg(short, long, default_value = "8080")]
547        port: u16,
548    },
549
550    /// Install FraiseQL mutation helper functions
551    ///
552    /// Installs SQL helper functions (fraiseql.mutation_ok, fraiseql.mutation_err, etc.)
553    /// to reduce boilerplate when writing mutation functions under the v2.2.0 protocol.
554    /// The helpers are installed in the `fraiseql` schema, which is owned by FraiseQL's
555    /// database role.
556    #[command(after_help = "\
557EXAMPLES:
558    fraiseql setup --database postgres://localhost/mydb
559    fraiseql setup --dry-run
560    fraiseql setup  # Uses DATABASE_URL or [database].url from fraiseql.toml")]
561    Setup {
562        /// Database connection URL (or use DATABASE_URL env var, or [database].url in
563        /// fraiseql.toml)
564        #[arg(long, value_name = "DATABASE_URL")]
565        database: Option<String>,
566
567        /// Print SQL without applying changes
568        #[arg(long)]
569        dry_run: bool,
570    },
571
572    /// Inspect schema metadata from a running FraiseQL server
573    ///
574    /// Fetches field-level security metadata (encryption, scope requirements, deny actions)
575    /// from the server's `/api/v1/schema/metadata` endpoint and displays it as a table.
576    #[command(after_help = "\
577EXAMPLES:
578    fraiseql schema metadata
579    fraiseql schema metadata --server http://localhost:8080
580    fraiseql schema metadata --server https://api.example.com --token mytoken")]
581    Schema {
582        #[command(subcommand)]
583        command: SchemaCommands,
584    },
585
586    /// Run diagnostic checks for common FraiseQL setup problems
587    ///
588    /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
589    /// and cache/auth coherence. Prints a color-coded report and exits 0 if
590    /// all checks pass or 1 if any check fails (warnings do not fail).
591    #[command(after_help = "\
592EXAMPLES:
593    fraiseql doctor
594    fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
595    fraiseql doctor --db-url postgres://user:pass@host:5432/db
596    fraiseql doctor --json")]
597    Doctor {
598        /// Path to fraiseql.toml configuration file.
599        #[arg(long, default_value = "fraiseql.toml")]
600        config: std::path::PathBuf,
601
602        /// Path to schema.compiled.json.
603        #[arg(long, default_value = "schema.compiled.json")]
604        schema: std::path::PathBuf,
605
606        /// Override DATABASE_URL for the connectivity check.
607        #[arg(long)]
608        db_url: Option<String>,
609
610        /// Run the live-database passes against this PostgreSQL database: the
611        /// change-log contract drift check (compares core.tb_entity_change_log
612        /// against the shipped contract — #380) and the PL/pgSQL
613        /// body-resolution pass (reports internal calls that no longer resolve
614        /// — #409).
615        ///
616        /// The body-resolution pass requires the `plpgsql_check` extension and
617        /// skips with a hint when it is unavailable.
618        #[arg(long, value_name = "DATABASE_URL")]
619        against_db: Option<String>,
620
621        /// Schemas to scan in the body-resolution pass (comma-separated).
622        #[arg(
623            long,
624            value_name = "SCHEMAS",
625            value_delimiter = ',',
626            default_value = "public"
627        )]
628        schemas: Vec<String>,
629
630        /// Output machine-readable JSON (for CI integration).
631        #[arg(long)]
632        json: bool,
633    },
634
635    /// Change-log performance observability (#392)
636    ///
637    /// Reads the framework-owned change-log (`core.v_entity_change_log`) and turns
638    /// it into operator-facing forensics: latency regressions between two time
639    /// windows, and ad-hoc reads of the slowest mutations / duration completeness
640    /// / per-operation percentiles.
641    Perf {
642        #[command(subcommand)]
643        command: PerfCommands,
644    },
645}
646
647/// `fraiseql perf` subcommands.
648#[derive(Subcommand)]
649pub(crate) enum PerfCommands {
650    /// Flag mutations whose latency regressed between a baseline and a recent
651    /// window, per `(object_type, modification_type)`.
652    ///
653    /// Only rows carrying the current `duration_calc_version` are compared, and
654    /// latency is never aggregated across modification types — so a shift in the
655    /// operation mix cannot mask a regression as a false improvement. Exits 0 even
656    /// when regressions are found (it is a report); pass `--fail-on-regression`
657    /// for CI gating.
658    RegressionScan {
659        /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
660        #[arg(long, value_name = "DATABASE_URL")]
661        database: Option<String>,
662
663        /// Recent window size, in days.
664        #[arg(long, default_value_t = 7)]
665        recent_days: i64,
666
667        /// Baseline window size (immediately before the recent window), in days.
668        #[arg(long, default_value_t = 7)]
669        baseline_days: i64,
670
671        /// Minimum comparable samples required on each side; thinner groups are
672        /// skipped rather than flagged.
673        #[arg(long, default_value_t = 20)]
674        min_samples: usize,
675
676        /// Minimum p50 increase to flag, in percent.
677        #[arg(long, default_value_t = 25.0)]
678        threshold_pct: f64,
679
680        /// Minimum absolute p50 increase to flag, in milliseconds (the noise floor).
681        #[arg(long, default_value_t = 5.0)]
682        min_delta_ms: f64,
683
684        /// Restrict the scan to a single object_type.
685        #[arg(long, value_name = "TYPE")]
686        object_type: Option<String>,
687
688        /// Exit non-zero when any regression is found (for CI gating).
689        #[arg(long)]
690        fail_on_regression: bool,
691    },
692
693    /// Ad-hoc forensic reads of the change-log.
694    Explore {
695        #[command(subcommand)]
696        command: PerfExploreCommands,
697    },
698}
699
700/// `fraiseql perf explore` subcommands — read-only views over comparable (v2)
701/// change-log rows in a trailing window.
702#[derive(Subcommand)]
703pub(crate) enum PerfExploreCommands {
704    /// The slowest individual mutations, slowest first.
705    Slowest {
706        /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
707        #[arg(long, value_name = "DATABASE_URL")]
708        database: Option<String>,
709
710        /// Trailing window to scan, in days.
711        #[arg(long, default_value_t = 7)]
712        days: i32,
713
714        /// Restrict to a single object_type.
715        #[arg(long, value_name = "TYPE")]
716        object_type: Option<String>,
717
718        /// Maximum rows to return.
719        #[arg(long, default_value_t = 20)]
720        limit: usize,
721    },
722
723    /// `duration_ms` completeness (NULL / pre-fix rates), per operation.
724    NullRate {
725        /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
726        #[arg(long, value_name = "DATABASE_URL")]
727        database: Option<String>,
728
729        /// Trailing window to scan, in days.
730        #[arg(long, default_value_t = 7)]
731        days: i32,
732
733        /// Restrict to a single object_type.
734        #[arg(long, value_name = "TYPE")]
735        object_type: Option<String>,
736    },
737
738    /// Latency percentiles (p50/p95/p99/max), per operation.
739    Summary {
740        /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
741        #[arg(long, value_name = "DATABASE_URL")]
742        database: Option<String>,
743
744        /// Trailing window to scan, in days.
745        #[arg(long, default_value_t = 7)]
746        days: i32,
747
748        /// Restrict to a single object_type.
749        #[arg(long, value_name = "TYPE")]
750        object_type: Option<String>,
751    },
752}
753
754#[derive(Subcommand)]
755pub(crate) enum ValidateCommands {
756    /// Validate that declared fact tables match database schema
757    Facts {
758        /// Schema.json file path
759        #[arg(short, long, value_name = "SCHEMA")]
760        schema: String,
761
762        /// Database connection string
763        #[arg(short, long, value_name = "DATABASE_URL")]
764        database: String,
765    },
766}
767
768#[derive(Subcommand)]
769pub(crate) enum GenerateClientCommands {
770    /// Generate a TypeScript client (interfaces + typed query/mutation functions).
771    Typescript {
772        /// Path to schema.compiled.json (auto-detected from conventional
773        /// locations if omitted).
774        #[arg(long, value_name = "SCHEMA")]
775        schema: Option<std::path::PathBuf>,
776
777        /// Output directory for the generated client.
778        #[arg(long, value_name = "DIR")]
779        out: std::path::PathBuf,
780
781        /// Overwrite an existing generated client in the output directory.
782        #[arg(long, default_value_t = false)]
783        force: bool,
784    },
785}
786
787#[derive(Subcommand)]
788pub(crate) enum FederationCommands {
789    /// Export federation graph
790    Graph {
791        /// Path to schema.compiled.json
792        #[arg(value_name = "SCHEMA")]
793        schema: String,
794
795        /// Output format (json, dot, mermaid)
796        #[arg(short, long, value_name = "FORMAT", default_value = "json")]
797        format: String,
798    },
799
800    /// Validate subgraph composition
801    Check {
802        /// Path to local schema.compiled.json
803        #[arg(value_name = "SCHEMA")]
804        schema: String,
805
806        /// Path to supergraph schema for composition validation
807        #[arg(short, long, value_name = "SUPERGRAPH")]
808        against: Option<String>,
809
810        /// Output result as JSON
811        #[arg(long)]
812        json: bool,
813    },
814}
815
816#[derive(Subcommand)]
817pub(crate) enum IntrospectCommands {
818    /// Introspect database for fact tables (tf_* tables)
819    Facts {
820        /// Database connection string
821        #[arg(short, long, value_name = "DATABASE_URL")]
822        database: String,
823
824        /// Output format (python, json)
825        #[arg(short, long, value_name = "FORMAT", default_value = "python")]
826        format: String,
827    },
828}
829
830#[derive(Subcommand)]
831pub(crate) enum SchemaCommands {
832    /// Display field-level security metadata from a running server
833    Metadata {
834        /// Server base URL
835        #[arg(
836            short,
837            long,
838            value_name = "URL",
839            default_value = "http://localhost:8080"
840        )]
841        server: String,
842
843        /// Bearer token for authentication
844        #[arg(short, long, value_name = "TOKEN")]
845        token: Option<String>,
846    },
847}
848
849#[derive(Subcommand)]
850pub(crate) enum MigrateCommands {
851    /// Apply pending migrations
852    Up {
853        /// Database connection URL
854        #[arg(long, value_name = "DATABASE_URL")]
855        database: Option<String>,
856
857        /// Migration directory
858        #[arg(long, value_name = "DIR")]
859        dir: Option<String>,
860    },
861
862    /// Roll back migrations
863    Down {
864        /// Database connection URL
865        #[arg(long, value_name = "DATABASE_URL")]
866        database: Option<String>,
867
868        /// Migration directory
869        #[arg(long, value_name = "DIR")]
870        dir: Option<String>,
871
872        /// Number of migrations to roll back
873        #[arg(long, default_value = "1")]
874        steps: u32,
875    },
876
877    /// Show migration status
878    Status {
879        /// Database connection URL
880        #[arg(long, value_name = "DATABASE_URL")]
881        database: Option<String>,
882
883        /// Migration directory
884        #[arg(long, value_name = "DIR")]
885        dir: Option<String>,
886    },
887
888    /// Create a new migration file
889    Create {
890        /// Migration name
891        #[arg(value_name = "NAME")]
892        name: String,
893
894        /// Migration directory
895        #[arg(long, value_name = "DIR")]
896        dir: Option<String>,
897    },
898
899    /// Generate a new migration from schema diff
900    ///
901    /// Delegates to `confiture migrate generate`.
902    /// Creates a timestamped migration file.
903    Generate {
904        /// Migration name
905        #[arg(value_name = "NAME")]
906        name: String,
907
908        /// Migration directory
909        #[arg(long, value_name = "DIR")]
910        dir: Option<String>,
911    },
912
913    /// Validate migration files for naming, idempotency, and drift
914    ///
915    /// Delegates to `confiture migrate validate`.
916    /// Checks naming conventions, idempotency, and schema drift.
917    Validate {
918        /// Migration directory
919        #[arg(long, value_name = "DIR")]
920        dir: Option<String>,
921    },
922
923    /// Pre-deploy safety check on pending migrations
924    ///
925    /// Delegates to `confiture migrate preflight`.
926    /// Verifies reversibility, detects non-transactional statements, checks checksums.
927    Preflight {
928        /// Migration directory
929        #[arg(long, value_name = "DIR")]
930        dir: Option<String>,
931    },
932}