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