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