Skip to main content

fraiseql_cli/
cli.rs

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