Skip to main content

pharos_native/runtime/
database.rs

1#![allow(clippy::too_many_arguments)]
2
3pub mod operations;
4
5use super::json_commands::manifest_database_contract_path;
6use pharos_runtime_database::operations::DatabaseError;
7use std::path::Path;
8
9/// Performs dynamic relational backend.
10///
11/// # Parameters
12///
13/// - `app_root`: Input consumed by [`crate::runtime::database::dynamic_relational_backend`].
14/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::dynamic_relational_backend`].
15///
16/// # Returns
17///
18/// Returns [`Some`] when [`crate::runtime::database::dynamic_relational_backend`] can produce a value and [`None`] otherwise.
19///
20/// # Examples
21///
22/// ```rust,ignore
23/// let _result = pharos_native::runtime::database::dynamic_relational_backend(std::path::Path::new("apps/dev_docs"), todo!());
24/// ```
25///
26/// # Related
27///
28/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
29pub fn dynamic_relational_backend(
30    app_root: &Path,
31    runtime_conf_path: Option<&Path>,
32) -> Option<String> {
33    if !relational_runtime_authority_enabled(app_root) {
34        return None;
35    }
36    load_cli_backend_config(app_root, runtime_conf_path, None)
37        .and_then(|config| operations::resolve(&config).ok())
38        .map(|backend| backend.backend_name().to_owned())
39}
40
41/// Loads cli backend config.
42///
43/// # Parameters
44///
45/// - `app_root`: Input consumed by [`crate::runtime::database::load_cli_backend_config`].
46/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::load_cli_backend_config`].
47/// - `app_id`: Input consumed by [`crate::runtime::database::load_cli_backend_config`].
48///
49/// # Returns
50///
51/// Returns [`Some`] when [`crate::runtime::database::load_cli_backend_config`] can produce a value and [`None`] otherwise.
52///
53/// # Examples
54///
55/// ```rust,ignore
56/// let _result = pharos_native::runtime::database::load_cli_backend_config(std::path::Path::new("apps/dev_docs"), todo!(), todo!());
57/// ```
58///
59/// # Related
60///
61/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
62pub fn load_cli_backend_config(
63    app_root: &Path,
64    runtime_conf_path: Option<&Path>,
65    app_id: Option<&str>,
66) -> Option<pharos_runtime_database::DbBackendConfig> {
67    checked_backend_config(app_root, runtime_conf_path, app_id).ok()
68}
69
70/// Resolves configuration without confusing invalid settings with absent records.
71pub fn checked_backend_config(
72    app_root: &Path,
73    runtime_conf_path: Option<&Path>,
74    app_id: Option<&str>,
75) -> Result<pharos_runtime_database::DbBackendConfig, DatabaseError> {
76    if app_root.as_os_str().is_empty() {
77        return Err(DatabaseError::InvalidInput);
78    }
79    pharos_runtime_database::DbBackendConfig::from_lookup(
80        app_id.filter(|value| !value.is_empty()).unwrap_or("app"),
81        |key| super::cache::dynamic_config_allowlist_value(app_root, runtime_conf_path, key),
82    )
83    .map_err(|_| DatabaseError::InvalidContract)
84}
85/// Loads the declared logical database contract; unsupported documents fail closed.
86pub fn selected_logical_contract(
87    app_root: &Path,
88    app_id: &str,
89) -> Result<
90    Option<pharos_runtime_database::contract::AppDataContract>,
91    pharos_runtime_database::operations::DatabaseError,
92> {
93    use pharos_runtime_database::operations::{DatabaseError, valid_identifier};
94    if !valid_identifier(app_id) {
95        return Err(DatabaseError::InvalidInput);
96    }
97    let Some(relative) = manifest_database_contract_path(app_root) else {
98        return Ok(None);
99    };
100    let relative_path = Path::new(&relative);
101    if relative_path.extension().and_then(|value| value.to_str()) != Some("json") {
102        return Err(DatabaseError::InvalidContract);
103    }
104    if relative_path
105        .components()
106        .any(|component| !matches!(component, std::path::Component::Normal(_)))
107    {
108        return Err(DatabaseError::InvalidContract);
109    }
110    let root = app_root
111        .canonicalize()
112        .map_err(|_| DatabaseError::InvalidContract)?;
113    let path = root.join(relative_path);
114    let canonical = path
115        .canonicalize()
116        .map_err(|_| DatabaseError::InvalidContract)?;
117    if !canonical.starts_with(&root) || canonical != path {
118        return Err(DatabaseError::InvalidContract);
119    }
120    pharos_runtime_database::contract::load_contract(&path, app_id).map(Some)
121}
122
123/// One request-scoped handle to an app's configured logical database.
124pub struct LogicalAppStore {
125    pub config: pharos_runtime_database::DbBackendConfig,
126    pub schema: pharos_runtime_database::operations::AppSchema,
127    pub adapter: std::sync::Arc<dyn pharos_runtime_database::operations::DatabaseOperations>,
128}
129
130/// Resolves the selected app contract and backend once for a logical operation.
131pub fn logical_app_store(
132    app_root: &Path,
133    runtime_conf_path: Option<&Path>,
134    app_id: &str,
135) -> Result<LogicalAppStore, pharos_runtime_database::operations::DatabaseError> {
136    logical_app_store_at(app_root, app_root, runtime_conf_path, app_id)
137}
138
139/// Reads runtime schema from the compiled artifact while resolving configuration from source.
140pub fn runtime_logical_app_store(
141    app: &crate::config::AppRuntime,
142) -> Result<LogicalAppStore, DatabaseError> {
143    let source = app
144        .app_root
145        .as_deref()
146        .or(app.artifact_dir.as_deref())
147        .map(Path::new)
148        .ok_or(DatabaseError::InvalidContract)?;
149    let artifact = app.artifact_dir.as_deref().map(Path::new).unwrap_or(source);
150    let app_id = app
151        .app_id
152        .as_deref()
153        .ok_or(DatabaseError::InvalidContract)?;
154    logical_app_store_at(
155        source,
156        artifact,
157        app.runtime_conf_path.as_deref().map(Path::new),
158        app_id,
159    )
160}
161
162/// Opens one logical store with separate configuration and compiled-contract roots.
163fn logical_app_store_at(
164    app_root: &Path,
165    contract_root: &Path,
166    runtime_conf_path: Option<&Path>,
167    app_id: &str,
168) -> Result<LogicalAppStore, DatabaseError> {
169    let contract =
170        selected_logical_contract(contract_root, app_id)?.ok_or(DatabaseError::InvalidContract)?;
171    logical_store_for_schema(app_root, runtime_conf_path, contract.schema)
172}
173
174/// Resolves one configured adapter for a schema already validated through the JSON loader.
175fn logical_store_for_schema(
176    app_root: &Path,
177    runtime_conf_path: Option<&Path>,
178    schema: pharos_runtime_database::operations::AppSchema,
179) -> Result<LogicalAppStore, DatabaseError> {
180    let config = checked_backend_config(app_root, runtime_conf_path, Some(&schema.app_id))?;
181    let adapter = operations::resolve(&config)?;
182    Ok(LogicalAppStore {
183        config,
184        schema,
185        adapter,
186    })
187}
188
189/// Compiles only the selected source contract before opening an explicit operator store.
190pub fn source_app_operations_store(
191    app_root: &Path,
192    runtime_conf_path: Option<&Path>,
193    app_id: &str,
194    build_dir: Option<&Path>,
195) -> Result<LogicalAppStore, DatabaseError> {
196    let contract =
197        source_selected_logical_contract(app_root, runtime_conf_path, app_id, build_dir)?
198            .ok_or(DatabaseError::InvalidContract)?;
199    logical_store_for_schema(app_root, runtime_conf_path, contract.schema)
200}
201
202/// Materializes manifest-selected source through the compiler, then uses the JSON-only loader.
203/// Database operators need a validated contract artifact, not unrelated UI build inputs.
204pub fn source_selected_logical_contract(
205    app_root: &Path,
206    runtime_conf_path: Option<&Path>,
207    app_id: &str,
208    build_dir: Option<&Path>,
209) -> Result<Option<pharos_runtime_database::contract::AppDataContract>, DatabaseError> {
210    if app_root.join("pharos.app.json").is_file() {
211        return selected_logical_contract(app_root, app_id);
212    }
213    let manifest = pharos_runtime_core::yaml::parse_yaml_file(&app_root.join("pharos.app.yml"))
214        .ok_or(DatabaseError::InvalidContract)?;
215    if manifest.pointer("/database/migration_contract").is_none() {
216        return Ok(None);
217    }
218    let configured_build = pharos_runtime_core::config::dynamic_config_effective_value(
219        app_root,
220        runtime_conf_path,
221        "build_dir",
222    )
223    .unwrap_or_else(|| "dist".to_owned());
224    let build_dir = build_dir.unwrap_or(Path::new(&configured_build));
225    // Standalone contracts can use shared configuration without an app.conf.
226    let artifact = if app_root.join("app.conf").is_file() {
227        pharos_runtime_core::config::effective_app_artifact_dir(app_root, build_dir, app_id)
228    } else {
229        pharos_runtime_core::config::app_artifact_dir_from_build_dir(build_dir, app_id)
230    }
231    .ok_or(DatabaseError::InvalidContract)?;
232    let compiled = pharos_runtime_compiler::database::compile_selected(
233        app_root,
234        Path::new(&artifact),
235        app_id,
236    )?;
237    pharos_runtime_database::contract::load_contract(&compiled, app_id).map(Some)
238}
239
240/// Checks a loaded app's compiled schema without parsing or compiling source YAML on requests.
241pub fn runtime_database_ready(app: &crate::config::AppRuntime) -> Option<bool> {
242    let store = runtime_logical_app_store(app).ok()?;
243    Some(store.adapter.ready(&store.config, &store.schema).is_ok())
244}
245
246/// Opens the same manifest-selected logical store used by declared features.
247pub fn app_operations_store(
248    app_root: &Path,
249    runtime_conf_path: Option<&Path>,
250    app_id: &str,
251) -> Result<LogicalAppStore, pharos_runtime_database::operations::DatabaseError> {
252    logical_app_store(app_root, runtime_conf_path, app_id)
253}
254
255/// Returns an app's external file only when its configured adapter owns a local artifact.
256/// Remote databases and file-backed apps do not claim an unused inherited database path.
257pub fn app_local_database_artifact_path(
258    app_root: &Path,
259    runtime_conf_path: Option<&Path>,
260    app_id: &str,
261) -> Result<Option<std::path::PathBuf>, pharos_runtime_database::operations::DatabaseError> {
262    use pharos_runtime_database::operations::DatabaseError;
263    if !relational_runtime_authority_enabled(app_root) {
264        return Ok(None);
265    }
266    let config = checked_backend_config(app_root, runtime_conf_path, Some(app_id))?;
267    let adapter = operations::resolve(&config)?;
268    if adapter.local_artifact_format().is_none() {
269        return Ok(None);
270    }
271    config
272        .sqlite_path
273        .map(Some)
274        .ok_or(DatabaseError::InvalidInput)
275}
276
277/// Checks stored credentials without treating failed storage as an invalid password.
278pub fn dynamic_relational_authenticate_user(
279    app_root: &Path,
280    runtime_conf_path: Option<&Path>,
281    app_id: Option<&str>,
282    username: &str,
283    password: &str,
284) -> Result<Option<i64>, DatabaseError> {
285    let store = app_operations_store(
286        app_root,
287        runtime_conf_path,
288        app_id.ok_or(DatabaseError::InvalidInput)?,
289    )?;
290    let Some(credentials) =
291        store
292            .adapter
293            .user_credentials(&store.config, &store.schema, username)?
294    else {
295        return Ok(None);
296    };
297    Ok((credentials.is_active
298        && crate::runtime::dynamic::verify_password_hash(
299            &credentials.password_hash,
300            password,
301            Some(username),
302        ))
303    .then_some(credentials.user_id))
304}
305
306/// Updates an account credential through the configured database adapter.
307pub fn dynamic_relational_update_user_password(
308    app_root: &Path,
309    runtime_conf_path: Option<&Path>,
310    app_id: Option<&str>,
311    user_id: i64,
312    password_hash: &str,
313) -> Result<(), pharos_runtime_database::operations::DatabaseError> {
314    let config = checked_backend_config(app_root, runtime_conf_path, app_id)?;
315    let adapter = operations::resolve(&config)?;
316    adapter.update_user_password(&config, user_id, password_hash)
317}
318
319/// Persists a browser session through the selected adapter and retains storage errors.
320pub fn dynamic_relational_issue_session(
321    app_root: &Path,
322    runtime_conf_path: Option<&Path>,
323    app_id: Option<&str>,
324    user_id: i64,
325    session_token_hash: &str,
326    expires_at: &str,
327) -> Result<(), DatabaseError> {
328    let config = checked_backend_config(app_root, runtime_conf_path, app_id)?;
329    operations::resolve(&config)?.issue_session(&config, user_id, session_token_hash, expires_at)
330}
331
332/// Loads a bounded management list and preserves configuration or storage failures.
333pub fn dynamic_relational_managed_businesses(
334    app_root: &Path,
335    runtime_conf_path: Option<&Path>,
336    app_id: Option<&str>,
337    user_id: i64,
338    limit: usize,
339) -> Result<serde_json::Value, DatabaseError> {
340    let store = app_operations_store(
341        app_root,
342        runtime_conf_path,
343        app_id.ok_or(DatabaseError::InvalidInput)?,
344    )?;
345    let businesses = pharos_runtime_business::managed_businesses(
346        store.adapter.as_ref(),
347        &store.config,
348        &store.schema,
349        user_id,
350        limit,
351    )?;
352    serde_json::to_value(businesses).map_err(|_| DatabaseError::StorageFailure)
353}
354
355/// Executes configured authority admission while retaining storage failures.
356pub fn dynamic_relational_actor_tenant_roles(
357    app_root: &Path,
358    runtime_conf_path: Option<&Path>,
359    app_id: Option<&str>,
360    user_id: i64,
361) -> Result<Vec<String>, DatabaseError> {
362    let store = app_operations_store(
363        app_root,
364        runtime_conf_path,
365        app_id.ok_or(DatabaseError::InvalidInput)?,
366    )?;
367    pharos_runtime_business::actor_tenant_roles(
368        store.adapter.as_ref(),
369        &store.config,
370        &store.schema,
371        user_id,
372    )
373}
374
375/// Executes configured authority admission while retaining storage failures.
376pub fn dynamic_relational_actor_manages_business(
377    app_root: &Path,
378    runtime_conf_path: Option<&Path>,
379    app_id: Option<&str>,
380    user_id: i64,
381    business_slug: &str,
382) -> Result<bool, DatabaseError> {
383    let store = app_operations_store(
384        app_root,
385        runtime_conf_path,
386        app_id.ok_or(DatabaseError::InvalidInput)?,
387    )?;
388    pharos_runtime_business::actor_manages_business(
389        store.adapter.as_ref(),
390        &store.config,
391        &store.schema,
392        user_id,
393        business_slug,
394    )
395}
396
397/// Checks appointment ownership in the configured primary backend.
398/// Executes configured authority admission while retaining storage failures.
399pub fn dynamic_relational_actor_owns_appointment(
400    app_root: &Path,
401    runtime_conf_path: Option<&Path>,
402    app_id: Option<&str>,
403    user_id: i64,
404    appointment_id: i64,
405) -> Result<bool, DatabaseError> {
406    let store = app_operations_store(
407        app_root,
408        runtime_conf_path,
409        app_id.ok_or(DatabaseError::InvalidInput)?,
410    )?;
411    pharos_runtime_business::actor_owns_appointment(
412        store.adapter.as_ref(),
413        &store.config,
414        &store.schema,
415        user_id,
416        appointment_id,
417    )
418}
419
420/// Appends one rate-limit audit event through the configured relational backend.
421pub fn dynamic_relational_append_rate_limit_event(
422    app_root: &Path,
423    runtime_conf_path: Option<&Path>,
424    app_id: Option<&str>,
425    policy_key: &str,
426    bucket_key: &str,
427    outcome: &str,
428) -> Result<(), DatabaseError> {
429    let config = checked_backend_config(app_root, runtime_conf_path, app_id)?;
430    operations::resolve(&config)?.append_rate_limit_event(&config, policy_key, bucket_key, outcome)
431}
432
433/// Admits one request through the configured relational rate-limit backend when available.
434pub fn dynamic_relational_admit_rate_limited_request(
435    app_root: &Path,
436    runtime_conf_path: Option<&Path>,
437    app_id: Option<&str>,
438    policy_key: &str,
439    bucket_key: &str,
440    maximum: usize,
441    window_seconds: u64,
442) -> Result<bool, DatabaseError> {
443    let config = checked_backend_config(app_root, runtime_conf_path, app_id)?;
444    operations::resolve(&config)?.admit_rate_limited_request(
445        &config,
446        policy_key,
447        bucket_key,
448        maximum,
449        window_seconds,
450    )
451}
452
453/// Performs dynamic relational runtime export JSON.
454///
455/// # Parameters
456///
457/// - `app_root`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_export_json`].
458/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_export_json`].
459/// - `app_id`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_export_json`].
460///
461/// # Returns
462///
463/// Returns [`Some`] when [`crate::runtime::database::dynamic_relational_runtime_export_json`] can produce a value and [`None`] otherwise.
464///
465/// # Examples
466///
467/// ```rust,ignore
468/// let _result = pharos_native::runtime::database::dynamic_relational_runtime_export_json(std::path::Path::new("apps/dev_docs"), todo!(), todo!());
469/// ```
470///
471/// # Related
472///
473/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
474pub fn dynamic_relational_runtime_export_json(
475    app_root: &Path,
476    runtime_conf_path: Option<&Path>,
477    app_id: Option<&str>,
478) -> Option<String> {
479    let manifest = super::declarative::declarative_source_load_fixture(app_root, "pharos.app.yml")?;
480    let app_id = app_id.or_else(|| manifest.get("app_id").and_then(serde_json::Value::as_str))?;
481    let contract =
482        source_selected_logical_contract(app_root, runtime_conf_path, app_id, None).ok()??;
483    let store =
484        logical_store_for_schema(app_root, runtime_conf_path, contract.schema.clone()).ok()?;
485    pharos_runtime_database::snapshot::DatabaseSnapshot::capture(
486        store.adapter.as_ref(),
487        &store.config,
488        &contract,
489    )
490    .ok()?
491    .encode()
492    .ok()
493}
494
495/// Performs dynamic relational runtime audit count.
496///
497/// # Parameters
498///
499/// - `app_root`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_audit_count`].
500/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_audit_count`].
501/// - `app_id`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_audit_count`].
502///
503/// # Returns
504///
505/// Returns [`Some`] when [`crate::runtime::database::dynamic_relational_runtime_audit_count`] can produce a value and [`None`] otherwise.
506///
507/// # Examples
508///
509/// ```rust,ignore
510/// let _result = pharos_native::runtime::database::dynamic_relational_runtime_audit_count(std::path::Path::new("apps/dev_docs"), todo!(), todo!());
511/// ```
512///
513/// # Related
514///
515/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
516pub fn dynamic_relational_runtime_audit_count(
517    app_root: &Path,
518    runtime_conf_path: Option<&Path>,
519    app_id: Option<&str>,
520) -> Option<String> {
521    let manifest = super::declarative::declarative_source_load_fixture(app_root, "pharos.app.yml")?;
522    let app_id = app_id.or_else(|| manifest.get("app_id").and_then(serde_json::Value::as_str))?;
523    let store = source_app_operations_store(app_root, runtime_conf_path, app_id, None).ok()?;
524    let count = store
525        .adapter
526        .aggregate(
527            &store.config,
528            &store.schema,
529            &pharos_runtime_database::operations::AggregateQuery {
530                entity: "audit_event".to_owned(),
531                filter: None,
532                aggregate: pharos_runtime_database::operations::Aggregate::Count,
533            },
534        )
535        .ok()?
536        .as_u64()?;
537    Some(count.to_string())
538}
539
540/// Performs dynamic relational runtime import state.
541///
542/// # Parameters
543///
544/// - `app_root`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
545/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
546/// - `app_id`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
547/// - `state_path`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
548/// - `expected_audit_count`: Input consumed by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
549///
550/// # Returns
551///
552/// Returns the computed value produced by [`crate::runtime::database::dynamic_relational_runtime_import_state`].
553///
554/// # Examples
555///
556/// ```rust,ignore
557/// let _result = pharos_native::runtime::database::dynamic_relational_runtime_import_state(std::path::Path::new("apps/dev_docs"), todo!(), todo!(), todo!(), todo!());
558/// ```
559///
560/// # Related
561///
562/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
563pub fn dynamic_relational_runtime_import_state(
564    app_root: &Path,
565    runtime_conf_path: Option<&Path>,
566    app_id: Option<&str>,
567    state_path: &Path,
568    expected_audit_count: Option<&str>,
569) -> i32 {
570    let result = (|| -> Result<(), pharos_runtime_database::operations::DatabaseError> {
571        use pharos_runtime_database::operations::DatabaseError;
572        use std::io::Read;
573        let manifest =
574            super::declarative::declarative_source_load_fixture(app_root, "pharos.app.yml")
575                .ok_or(DatabaseError::InvalidContract)?;
576        let app_id = app_id
577            .or_else(|| manifest.get("app_id").and_then(serde_json::Value::as_str))
578            .ok_or(DatabaseError::InvalidContract)?;
579        let contract = source_selected_logical_contract(app_root, runtime_conf_path, app_id, None)?
580            .ok_or(DatabaseError::InvalidContract)?;
581        let store = logical_store_for_schema(app_root, runtime_conf_path, contract.schema.clone())?;
582        let file = std::fs::File::open(state_path).map_err(|_| DatabaseError::StorageFailure)?;
583        if !file
584            .metadata()
585            .map_err(|_| DatabaseError::StorageFailure)?
586            .is_file()
587        {
588            return Err(DatabaseError::InvalidInput);
589        }
590        let mut bytes = Vec::new();
591        file.take(pharos_runtime_database::snapshot::OPERATOR_LIMITS.max_bytes + 1)
592            .read_to_end(&mut bytes)
593            .map_err(|_| DatabaseError::StorageFailure)?;
594        let snapshot = pharos_runtime_database::snapshot::DatabaseSnapshot::decode(&bytes)?;
595        let counts = expected_audit_count
596            .map(|expected| {
597                expected
598                    .parse::<u64>()
599                    .map(|expected| pharos_runtime_database::snapshot::RestoreCount {
600                        entity: "audit_event".to_owned(),
601                        expected,
602                    })
603                    .map_err(|_| DatabaseError::InvalidInput)
604            })
605            .transpose()?
606            .into_iter()
607            .collect::<Vec<_>>();
608        snapshot.restore(store.adapter.as_ref(), &store.config, &contract, &counts)
609    })();
610    match result {
611        Ok(()) => 0,
612        Err(
613            pharos_runtime_database::operations::DatabaseError::InvalidInput
614            | pharos_runtime_database::operations::DatabaseError::InvalidContract,
615        ) => 64,
616        Err(pharos_runtime_database::operations::DatabaseError::Conflict) => 65,
617        Err(_) => 69,
618    }
619}
620
621/// Performs dynamic migration drift report.
622///
623/// # Parameters
624///
625/// - `app_root`: Input consumed by [`crate::runtime::database::dynamic_migration_drift_report`].
626/// - `runtime_conf_path`: Input consumed by [`crate::runtime::database::dynamic_migration_drift_report`].
627/// - `app_id`: Input consumed by [`crate::runtime::database::dynamic_migration_drift_report`].
628///
629/// # Returns
630///
631/// Returns [`Some`] when [`crate::runtime::database::dynamic_migration_drift_report`] can produce a value and [`None`] otherwise.
632///
633/// # Performance
634///
635/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::runtime::database::dynamic_migration_drift_report`].
636///
637/// # Examples
638///
639/// ```rust,ignore
640/// let _result = pharos_native::runtime::database::dynamic_migration_drift_report(std::path::Path::new("apps/dev_docs"), todo!(), todo!());
641/// ```
642///
643/// # Related
644///
645/// - [`crate::runtime::database`] for adjacent helpers that share the same runtime surface.
646pub fn dynamic_migration_drift_report(
647    app_root: &Path,
648    runtime_conf_path: Option<&Path>,
649    app_id: Option<&str>,
650) -> Option<serde_json::Value> {
651    let manifest = super::declarative::declarative_source_load_fixture(app_root, "pharos.app.yml")?;
652    let resolved_app_id =
653        app_id.or_else(|| manifest.get("app_id").and_then(serde_json::Value::as_str))?;
654    match source_selected_logical_contract(app_root, runtime_conf_path, resolved_app_id, None) {
655        Ok(Some(contract)) => {
656            let config =
657                load_cli_backend_config(app_root, runtime_conf_path, Some(resolved_app_id))?;
658            let adapter = operations::resolve(&config).ok()?;
659            let observed = adapter.migration_ledger(&config, resolved_app_id).ok()?;
660            let mut missing_migrations = Vec::new();
661            let mut applied_checksum_drift = Vec::new();
662            for migration in &contract.migrations {
663                match observed.get(&migration.id) {
664                    None => {
665                        missing_migrations.push(serde_json::Value::String(migration.id.clone()))
666                    }
667                    Some(checksum) if checksum != &migration.checksum => applied_checksum_drift
668                        .push(serde_json::json!({
669                            "migration_id": migration.id,
670                            "declared": migration.checksum,
671                            "observed": checksum,
672                        })),
673                    Some(_) => {}
674                }
675            }
676            let verified = adapter.verify_contract(&config, &contract).is_ok();
677            Some(serde_json::json!({
678                "schema": "pharos.migration-drift.v1",
679                "status": if verified { "verified" } else { "drifted" },
680                "backend": config.backend,
681                "missing_migrations": missing_migrations,
682                "applied_checksum_drift": applied_checksum_drift,
683                "declared_checksum_drift": [],
684                "interrupted_migrations": [],
685                "missing_live_tables": [],
686                "rollback_classification": [],
687            }))
688        }
689        Ok(None) | Err(_) => None,
690    }
691}
692
693/// Applies one manifest-selected logical contract through the configured adapter.
694fn manifest_database_app_id(app_root: &Path, hint: Option<&str>) -> Option<String> {
695    let manifest = super::declarative::declarative_source_load_fixture(app_root, "pharos.app.yml")?;
696    let declared = manifest.get("app_id")?.as_str()?.trim();
697    if declared.is_empty() || hint.is_some_and(|hint| hint != declared) {
698        return None;
699    }
700    Some(declared.to_owned())
701}
702
703/// Applies one manifest-selected logical contract through the configured adapter.
704pub fn dynamic_runtime_prepare(
705    app_root: &Path,
706    runtime_conf_path: Option<&Path>,
707    app_id: Option<&str>,
708) -> i32 {
709    let Some(app_id) = manifest_database_app_id(app_root, app_id) else {
710        return 66;
711    };
712    let Ok(Some(contract)) =
713        source_selected_logical_contract(app_root, runtime_conf_path, &app_id, None)
714    else {
715        return 66;
716    };
717    let Some(config) = load_cli_backend_config(app_root, runtime_conf_path, Some(&app_id)) else {
718        return 66;
719    };
720    let Ok(adapter) = operations::resolve(&config) else {
721        return 69;
722    };
723    match adapter
724        .ensure_database(&config, &contract.schema)
725        .and_then(|()| adapter.apply_contract(&config, &contract))
726    {
727        Ok(()) => 0,
728        Err(_) => 69,
729    }
730}
731
732/// Requires the declared logical schema on the configured adapter before readiness.
733pub fn dynamic_runtime_ready(
734    app_root: &Path,
735    runtime_conf_path: Option<&Path>,
736    app_id: Option<&str>,
737) -> Option<bool> {
738    let selected_id = manifest_database_app_id(app_root, app_id)?;
739    let config = load_cli_backend_config(app_root, runtime_conf_path, Some(&selected_id))?;
740    let Ok(Some(contract)) =
741        source_selected_logical_contract(app_root, runtime_conf_path, &selected_id, None)
742    else {
743        return Some(false);
744    };
745    let adapter = operations::resolve(&config).ok()?;
746    Some(adapter.ready(&config, &contract.schema).is_ok())
747}
748
749/// Verifies logical migration checksums for operator diagnostics.
750/// The ordinary runtime summary uses the cheaper readiness check above.
751pub fn dynamic_runtime_migrations_verified(
752    app_root: &Path,
753    runtime_conf_path: Option<&Path>,
754    app_id: Option<&str>,
755) -> Option<bool> {
756    let selected_id = manifest_database_app_id(app_root, app_id)?;
757    let config = load_cli_backend_config(app_root, runtime_conf_path, Some(&selected_id))?;
758    match source_selected_logical_contract(app_root, runtime_conf_path, &selected_id, None) {
759        Ok(Some(contract)) => {
760            let adapter = operations::resolve(&config).ok()?;
761            Some(adapter.verify_contract(&config, &contract).is_ok())
762        }
763        Ok(None) => Some(false),
764        Err(_) => Some(false),
765    }
766}
767
768/// Treats a declared database contract as durable authority, even if its file is
769/// missing or invalid. Contract validation must fail closed rather than silently
770/// switching the app to a stale JSON state file.
771pub(super) fn relational_runtime_authority_enabled(app_root: &Path) -> bool {
772    super::json_commands::manifest_declares_database(app_root)
773}
774
775#[cfg(test)]
776mod rate_limit_tests {
777    /// Bound resource IDs reject another actor while preserving the owner path.
778    #[test]
779    fn scoped_business_and_appointment_ownership() {
780        let directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
781            .join("tmp")
782            .join(format!(
783                "pharos-resource-owner-{}-{}",
784                std::process::id(),
785                std::time::SystemTime::now()
786                    .duration_since(std::time::UNIX_EPOCH)
787                    .expect("system clock")
788                    .as_nanos()
789            ));
790        let app_root = directory.join("app");
791        std::fs::create_dir_all(app_root.join("data/contracts"))
792            .expect("create app contract directory");
793        std::fs::write(
794            app_root.join("pharos.app.yml"),
795            "app_id: owner_test\ndatabase:\n  migration_contract: data/contracts/database.json\n",
796        )
797        .expect("write app manifest");
798        let database_path = directory.join("owner.sqlite3");
799        let runtime_conf = directory.join("runtime.conf");
800        std::fs::write(
801            &runtime_conf,
802            format!(
803                "db_backend=sqlite\ndb_path={}\n",
804                database_path.to_string_lossy()
805            ),
806        )
807        .expect("write isolated backend config");
808        use pharos_runtime_database::operations::{
809            AppSchema, Entity, Field, FieldType, Identity, Migration, SchemaChange,
810        };
811        let declarations = [
812            ("business", vec!["id", "slug", "owner_id"]),
813            ("tenant", vec!["id", "source_business_id", "is_active"]),
814            (
815                "tenant_membership",
816                vec![
817                    "id",
818                    "tenant_id",
819                    "user_id",
820                    "membership_status",
821                    "revoked_at",
822                    "role_id",
823                ],
824            ),
825            ("appointment", vec!["id", "user_id", "business_id"]),
826        ];
827        let entities = declarations
828            .into_iter()
829            .map(|(name, fields)| Entity {
830                name: name.to_owned(),
831                identity: Identity::AutoInteger("id".to_owned()),
832                fields: fields
833                    .into_iter()
834                    .map(|name| Field {
835                        name: name.to_owned(),
836                        kind: match name {
837                            "slug" | "membership_status" | "role_id" => FieldType::Text,
838                            "revoked_at" => FieldType::Timestamp,
839                            "is_active" => FieldType::Boolean,
840                            _ => FieldType::Integer,
841                        },
842                        nullable: name == "revoked_at",
843                        max_length: None,
844                        default: None,
845                        references: None,
846                        on_delete: None,
847                        constraints: Vec::new(),
848                    })
849                    .collect(),
850                unique: Vec::new(),
851                indexes: Vec::new(),
852                conditional_indexes: Vec::new(),
853            })
854            .collect::<Vec<_>>();
855        let schema = AppSchema {
856            app_id: "owner_test".to_owned(),
857            entities,
858        };
859        let mut changes = schema
860            .entities
861            .iter()
862            .cloned()
863            .map(SchemaChange::CreateEntity)
864            .collect::<Vec<_>>();
865        for (name, values) in [
866            (
867                "business",
868                serde_json::json!({"id":1,"slug":"owned","owner_id":10}),
869            ),
870            (
871                "business",
872                serde_json::json!({"id":2,"slug":"other","owner_id":20}),
873            ),
874            (
875                "tenant",
876                serde_json::json!({"id":1,"source_business_id":1,"is_active":true}),
877            ),
878            (
879                "tenant",
880                serde_json::json!({"id":2,"source_business_id":2,"is_active":true}),
881            ),
882            (
883                "tenant_membership",
884                serde_json::json!({"id":1,"tenant_id":1,"user_id":10,"membership_status":"active","revoked_at":null,"role_id":"business_owner"}),
885            ),
886            (
887                "tenant_membership",
888                serde_json::json!({"id":2,"tenant_id":2,"user_id":20,"membership_status":"active","revoked_at":null,"role_id":"business_owner"}),
889            ),
890            (
891                "appointment",
892                serde_json::json!({"id":1,"user_id":11,"business_id":1}),
893            ),
894            (
895                "appointment",
896                serde_json::json!({"id":2,"user_id":21,"business_id":2}),
897            ),
898        ] {
899            changes.push(SchemaChange::Seed {
900                entity: name.to_owned(),
901                values: values.as_object().expect("logical record").clone(),
902            });
903        }
904        let migration = Migration {
905            app_id: "owner_test".to_owned(),
906            id: "ownership_schema".to_owned(),
907            checksum: pharos_runtime_database::contract::migration_checksum(
908                "owner_test",
909                "ownership_schema",
910                &changes,
911            )
912            .expect("ownership checksum"),
913            changes,
914        };
915        let contract = pharos_runtime_database::contract::AppDataContract {
916            schema,
917            migrations: vec![migration],
918            legacy_adoption: None,
919        };
920        std::fs::write(
921            app_root.join("data/contracts/database.json"),
922            serde_json::to_vec(&contract).expect("logical contract JSON"),
923        )
924        .expect("logical ownership contract");
925        let config =
926            super::load_cli_backend_config(&app_root, Some(&runtime_conf), Some("owner_test"))
927                .expect("configured ownership database");
928        let adapter = super::operations::resolve(&config).expect("configured ownership adapter");
929        adapter
930            .apply_contract(&config, &contract)
931            .expect("logical ownership schema and records");
932        assert_eq!(
933            super::dynamic_relational_actor_manages_business(
934                &app_root,
935                Some(&runtime_conf),
936                Some("owner_test"),
937                10,
938                "owned"
939            ),
940            Ok(true)
941        );
942        assert_eq!(
943            super::dynamic_relational_actor_manages_business(
944                &app_root,
945                Some(&runtime_conf),
946                Some("owner_test"),
947                10,
948                "other"
949            ),
950            Ok(false)
951        );
952        assert_eq!(
953            super::dynamic_relational_actor_owns_appointment(
954                &app_root,
955                Some(&runtime_conf),
956                Some("owner_test"),
957                10,
958                1
959            ),
960            Ok(true)
961        );
962        assert_eq!(
963            super::dynamic_relational_actor_owns_appointment(
964                &app_root,
965                Some(&runtime_conf),
966                Some("owner_test"),
967                11,
968                1
969            ),
970            Ok(true)
971        );
972        assert_eq!(
973            super::dynamic_relational_actor_owns_appointment(
974                &app_root,
975                Some(&runtime_conf),
976                Some("owner_test"),
977                10,
978                2
979            ),
980            Ok(false)
981        );
982        std::fs::remove_dir_all(directory).expect("remove resource database directory");
983    }
984}