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