Skip to main content

sql_fun_core/
args.rs

1mod cli_sub_command;
2mod error;
3mod impl_metadata;
4mod impl_sql_fun_home;
5mod init_args;
6mod log_args;
7mod metadata_args;
8mod postgres;
9
10use std::{collections::HashMap, fmt::Display, path::PathBuf};
11
12use clap::Parser;
13
14use self::log_args::LoggingArgs;
15pub use self::{
16    cli_sub_command::CliSubCommand, error::SqlFunArgsError, init_args::InitializeArgs,
17    metadata_args::MetadataArgs, postgres::PostgresArgs,
18};
19
20use crate::{
21    HighlighterTheme, PostgresExtensionsCollection, SqlDialect, SqlFunMetadata, TerminalColor,
22    metadata::EngineVersion,
23};
24
25#[derive(Debug)]
26pub enum ConfigurationKind {
27    EngineVersion,
28}
29
30impl Display for ConfigurationKind {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            ConfigurationKind::EngineVersion => write!(f, "Database engine version"),
34        }
35    }
36}
37
38/// Common `sql-fun` CLI arguments and environment variables
39#[derive(clap::Parser, Debug, serde::Serialize, serde::Deserialize, Clone)]
40#[clap(name = "sql-fun", version, about, long_about = None)]
41pub struct SqlFunArgs {
42    #[command(flatten)]
43    metadata_args: MetadataArgs,
44
45    #[command(flatten)]
46    postgres_args: PostgresArgs,
47
48    #[command(flatten)]
49    log_args: LoggingArgs,
50
51    /// Output format (text or json)
52    #[clap(long, env = Self::SQL_FUN_OUTPUT_FORMAT, default_value = "text")]
53    output_format: String,
54
55    /// Terminal Color
56    #[clap(long, env = Self::SQL_FUN_TERM_COLOR, default_value = "auto")]
57    term_color: TerminalColor,
58
59    /// Syntax Highlighter Theme
60    ///
61    /// default values:
62    ///
63    /// when `--term-color` is `never` : then monochrome plain texts
64    /// when `--term-color` is `ansi` : `${SQL_FUN_HOME}/highlighter/default.toml`
65    ///
66    #[clap(long, env = Self::SQL_FUN_HIGHLIGHTER_THEME)]
67    highlighter_theme: Option<PathBuf>,
68
69    /// Verbose mode (equivalent to --log-level=debug)
70    #[clap(long)]
71    verbose: bool,
72
73    /// Disable loading built-in tables/views
74    #[clap(long, env = Self::SQL_FUN_NO_BUILTIN)]
75    no_builtin_tables: bool,
76
77    /// Subcommand to execute (internal or external)
78    #[clap(subcommand)]
79    #[serde(skip)]
80    command: Option<CliSubCommand>,
81}
82
83impl SqlFunArgs {
84    /// returns true for builtin subcommand
85    #[must_use]
86    pub fn is_builtin(&self) -> bool {
87        matches!(self.command, Some(CliSubCommand::Initialize(_)))
88    }
89
90    /// get SQL dialect
91    ///
92    /// # Errors
93    ///
94    /// Returns [`SqlFunArgsError`] when dialect resolution fails.
95    pub fn sql_dialect(&self, metadata: &SqlFunMetadata) -> Result<SqlDialect, SqlFunArgsError> {
96        self.metadata_args.sql_dialect(metadata)
97    }
98
99    /// get CTE catalog directory
100    ///
101    /// # Errors
102    ///
103    /// Propagates [`SqlFunArgsError`] from metadata lookups.
104    pub fn cte_catalog_dir(
105        &self,
106        metadata: &SqlFunMetadata,
107    ) -> Result<Option<PathBuf>, SqlFunArgsError> {
108        self.metadata_args.cte_catalog_dir(metadata)
109    }
110
111    /// get command
112    #[must_use]
113    pub fn command(&self) -> &Option<CliSubCommand> {
114        &self.command
115    }
116
117    /// get `PostgreSQL` version
118    ///
119    /// # Errors
120    ///
121    /// Returns [`SqlFunArgsError`] when the version cannot be determined.
122    pub fn postgres_version(
123        &self,
124        metadata: &SqlFunMetadata,
125    ) -> Result<EngineVersion, SqlFunArgsError> {
126        self.postgres_args.postgres_version(metadata)
127    }
128
129    /// get `PostgreSQL` search path
130    ///
131    /// # Errors
132    ///
133    /// Returns [`SqlFunArgsError`] when metadata retrieval fails.
134    pub fn postgres_search_path(
135        &self,
136        metadata: &SqlFunMetadata,
137    ) -> Result<Vec<String>, SqlFunArgsError> {
138        self.postgres_args.postgres_search_path(metadata)
139    }
140
141    /// get postgres extensions
142    ///
143    /// # Errors
144    ///
145    /// Propagates [`SqlFunArgsError`] if metadata lookup fails or the extension
146    /// configuration is invalid.
147    pub fn postgres_extensions(
148        &self,
149        metadata: &SqlFunMetadata,
150    ) -> Result<PostgresExtensionsCollection, SqlFunArgsError> {
151        self.postgres_args.postgres_extensions(metadata)
152    }
153
154    /// get builtin schema directory path
155    ///
156    /// # Errors
157    ///
158    /// Returns [`SqlFunArgsError`] if the builtin directory cannot be resolved.
159    pub fn builtin_info_dir(&self, metadata: &SqlFunMetadata) -> Result<PathBuf, SqlFunArgsError> {
160        if let Some(arg) = self.postgres_args.builtin_schema_dir() {
161            Ok(arg.clone())
162        } else {
163            let home = self.sql_fun_home()?;
164            let postgres_version = self.postgres_version(metadata)?;
165            Ok(postgres_version.definition_base_path(home))
166        }
167    }
168
169    /// set directory path of extensions
170    ///
171    /// # Errors
172    ///
173    /// Returns [`SqlFunArgsError`] when the path components cannot be derived.
174    pub fn postgres_extensions_dir(
175        &self,
176        metadata: &SqlFunMetadata,
177    ) -> Result<PathBuf, SqlFunArgsError> {
178        if let Some(ext_dir) = self.postgres_args.postgres_extensions_dir() {
179            Ok(ext_dir.clone())
180        } else {
181            let home = self.sql_fun_home()?;
182            let dialect = self.sql_dialect(metadata)?;
183            let ver = self.postgres_version(metadata)?;
184            Ok(home
185                .join(dialect.to_string())
186                .join(ver.to_string())
187                .join("extensions"))
188        }
189    }
190
191    const SQL_FUN_OUTPUT_FORMAT: &str = "SQL_FUN_OUTPUT_FORMAT";
192    const SQL_FUN_NO_BUILTIN: &str = "SQL_FUN_NO_BUILTIN";
193    const SQL_FUN_TERM_COLOR: &str = "SQL_FUN_TERM_COLOR";
194    const SQL_FUN_HIGHLIGHTER_THEME: &str = "SQL_FUN_HIGHLIGHTER_THEME";
195
196    /// get environemnts for child process
197    ///
198    /// # Errors
199    ///
200    /// Returns [`SqlFunArgsError`] if metadata fails to load or env generation
201    /// encounters an error.
202    pub fn environments(&self) -> Result<HashMap<String, String>, SqlFunArgsError> {
203        let mut result = HashMap::new();
204        let metadata_file = self.metadata_file()?;
205        let metadata = SqlFunMetadata::load_from(&metadata_file)?;
206
207        self.metadata_args.get_envs(&mut result, &metadata)?;
208        self.postgres_args.get_envs(&mut result, self, &metadata)?;
209        self.log_args.get_envs(&mut result);
210
211        result.insert(
212            Self::SQL_FUN_OUTPUT_FORMAT.to_string(),
213            self.output_format.clone(),
214        );
215
216        Ok(result)
217    }
218
219    /// get terminal color value
220    #[must_use]
221    pub fn term_color(&self) -> TerminalColor {
222        self.term_color.get_value()
223    }
224
225    /// get syntax highlighter theme
226    ///
227    /// when [`Self::term_color`] is never, returns [`HighlighterTheme::default`]
228    /// when [`Self::highlighter_theme`] is None, returns `${SQL_FUN_HOME}/highlighter/default.toml`
229    /// when [`Self::highlighter_theme`] is Some and relative path,
230    ///     returns ${SQL_FUN_HOME}/highlighter/specified-path
231    /// when [`Self::highlighter_theme`] is Some and absolute path,
232    ///     returns load from absolute path
233    ///
234    /// # Errors
235    ///
236    /// Returns [`SqlFunArgsError`] if faild load theme.
237    ///
238    pub fn highlighter_theme(&self) -> Result<HighlighterTheme, SqlFunArgsError> {
239        let term_color = self.term_color();
240        if term_color.is_never() {
241            Ok(HighlighterTheme::default())
242        } else {
243            let theme_path = if let Some(theme_path) = &self.highlighter_theme {
244                if theme_path.is_absolute() {
245                    theme_path.clone()
246                } else {
247                    let mut base = self.sql_fun_home()?;
248                    base.push("highlighter");
249                    base.push(theme_path);
250                    base
251                }
252            } else {
253                let mut path = self.sql_fun_home()?;
254                path.push("highlighter/default.toml");
255                path
256            };
257            Ok(HighlighterTheme::from_toml(theme_path)?)
258        }
259    }
260}
261
262/// deamon control args
263#[derive(clap::Parser, Debug, Clone, serde::Serialize, serde::Deserialize)]
264pub struct DaemonControlArgs {
265    /// daemon extension name
266    daemon_name: String,
267    /// daemon arguments
268    daemon_args: Vec<String>,
269}
270
271impl SqlFunArgs {
272    /// Initialize and parse arguments
273    #[must_use]
274    pub fn parse_args() -> Self {
275        let mut args = SqlFunArgs::parse();
276        if args.verbose {
277            args.log_args.set_log_level("debug");
278        }
279        args
280    }
281
282    /// Initialize for `proc_macro`
283    ///
284    /// # Errors
285    ///
286    /// Returns [`SqlFunArgsError`] if failed
287    pub fn try_from_env() -> Result<Self, SqlFunArgsError> {
288        let mut args = SqlFunArgs::try_parse_from(Vec::<String>::default())?;
289        if args.verbose {
290            args.log_args.set_log_level("debug");
291        }
292        Ok(args)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298
299    use clap::Parser;
300    use testresult::TestResult;
301
302    use crate::SqlFunArgs;
303
304    #[test]
305    pub fn test_schema_file() -> TestResult {
306        let args = SqlFunArgs::try_parse_from(vec![
307            "sqlfun",
308            "--sql-fun-home",
309            "../sql-fun-home",
310            "subcmd",
311        ])?;
312        let metadata_file = args.metadata_file()?;
313        let expect_file = metadata_file.with_file_name("sqlfun.develop.sql");
314        let mut created = false;
315        if !expect_file.exists() {
316            std::fs::write(&expect_file, "-- dummy sql file")?;
317            created = true;
318        }
319        let schema_file = args.schema_file(&metadata_file)?;
320        if created {
321            std::fs::remove_file(&expect_file)?;
322        }
323        assert_eq!(schema_file, expect_file);
324        Ok(())
325    }
326}