Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3752064
D2734.id6934.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D2734.id6934.diff
View Options
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,27 +1,27 @@
-[package]
-name = "fantoir-datasource"
-version = "0.1.0"
-edition = "2021"
-description = "Generates a Postgres table from FANTOIR raw file"
-authors = [
- "Sébastien Santoro <dereckson@espace-win.org>"
-]
-license = "BSD-2-Clause"
-
-[dependencies]
-
-[dependencies.async-scoped]
-version = "~0.7.1"
-features = ["use-tokio"]
-
-[dependencies.clap]
-version = "~4.0.32"
-features = ["derive"]
-
-[dependencies.sqlx]
-version = "~0.6.2"
-features = ["runtime-tokio-native-tls", "postgres", "chrono"]
-
-[dependencies.tokio]
-version = "~1.23.0"
-features = ["full"]
+[package]
+name = "fantoir-datasource"
+version = "0.1.0"
+edition = "2021"
+description = "Generates a Postgres table from FANTOIR raw file"
+authors = [
+ "Sébastien Santoro <dereckson@espace-win.org>"
+]
+license = "BSD-2-Clause"
+
+[dependencies]
+
+[dependencies.async-scoped]
+version = "~0.7.1"
+features = ["use-tokio"]
+
+[dependencies.clap]
+version = "~4.0.32"
+features = ["derive"]
+
+[dependencies.sqlx]
+version = "~0.6.2"
+features = ["runtime-tokio-native-tls", "postgres", "chrono"]
+
+[dependencies.tokio]
+version = "~1.23.0"
+features = ["full"]
diff --git a/src/commands/import.rs b/src/commands/import.rs
--- a/src/commands/import.rs
+++ b/src/commands/import.rs
@@ -1,84 +1,84 @@
-//! Import command for the fantoir-datasource tool.
-//!
-//! Import from FANTOIR file generated by the DGFIP
-
-use std::process::exit;
-
-use sqlx::PgPool;
-use tokio::fs::File;
-use tokio::io::{AsyncBufReadExt, BufReader};
-
-use crate::ImportArgs;
-use crate::db::*;
-use crate::fantoir::FantoirEntry;
-
-pub async fn import(args: &ImportArgs, database_url: &str) {
- let fd = File::open(&args.fantoir_file).await.expect("Can't open file.");
- let pool = connect_to_db(database_url).await;
-
- // Create/truncate table as needed and as allowed by options
- if let Err(error) = initialize_table(args, &pool).await {
- eprintln!("{}", &error);
- exit(1);
- }
-
- // Currently, async closures are unstable, see https://github.com/rust-lang/rust/issues/62290
- // They are also largely unimplemented. As such, this code doesn't follow HOF pattern.
- let mut buffer = BufReader::new(fd).lines();
- while let Ok(line) = buffer.next_line().await {
- if line.is_none() {
- break;
- }
- let line = line.unwrap();
-
- if line.len() < 90 {
- // This record is the header or describes a department or a commune
- continue;
- }
-
- if line.starts_with("9999999999") {
- // This record is the last of the database
- break;
- }
-
- FantoirEntry::parse_line(&line)
- .insert_to_db(&pool, &args.fantoir_table)
- .await
- }
-}
-
-async fn initialize_table(args: &ImportArgs, pool: &PgPool) -> Result<(), String> {
- if is_table_exists(pool, &args.fantoir_table).await {
- if is_table_empty(&pool, &args.fantoir_table).await {
- return Ok(());
- }
-
- if args.overwrite_table {
- truncate_table(&pool, &args.fantoir_table).await;
- return Ok(());
- }
-
- return Err(format!(
- "Table {} already exists and contains rows. To overwrite it, run the import tool with -t option.",
- &args.fantoir_table
- ));
- }
-
- if args.create_table {
- create_table(&pool, &args.fantoir_table).await;
- return Ok(());
- }
-
- Err(format!(
- "Table {} doesn't exist. To create it, run the import tool with -c option.",
- &args.fantoir_table
- ))
-}
-
-async fn create_table(pool: &PgPool, table: &str) {
- let queries = include_str!("../schema/fantoir.sql")
- .replace("/*table*/fantoir", table)
- .replace("/*index*/index_fantoir_", format!("index_{}_", table).as_ref());
-
- run_multiple_queries(pool, &queries).await;
-}
+//! Import command for the fantoir-datasource tool.
+//!
+//! Import from FANTOIR file generated by the DGFIP
+
+use std::process::exit;
+
+use sqlx::PgPool;
+use tokio::fs::File;
+use tokio::io::{AsyncBufReadExt, BufReader};
+
+use crate::ImportArgs;
+use crate::db::*;
+use crate::fantoir::FantoirEntry;
+
+pub async fn import(args: &ImportArgs, database_url: &str) {
+ let fd = File::open(&args.fantoir_file).await.expect("Can't open file.");
+ let pool = connect_to_db(database_url).await;
+
+ // Create/truncate table as needed and as allowed by options
+ if let Err(error) = initialize_table(args, &pool).await {
+ eprintln!("{}", &error);
+ exit(1);
+ }
+
+ // Currently, async closures are unstable, see https://github.com/rust-lang/rust/issues/62290
+ // They are also largely unimplemented. As such, this code doesn't follow HOF pattern.
+ let mut buffer = BufReader::new(fd).lines();
+ while let Ok(line) = buffer.next_line().await {
+ if line.is_none() {
+ break;
+ }
+ let line = line.unwrap();
+
+ if line.len() < 90 {
+ // This record is the header or describes a department or a commune
+ continue;
+ }
+
+ if line.starts_with("9999999999") {
+ // This record is the last of the database
+ break;
+ }
+
+ FantoirEntry::parse_line(&line)
+ .insert_to_db(&pool, &args.fantoir_table)
+ .await
+ }
+}
+
+async fn initialize_table(args: &ImportArgs, pool: &PgPool) -> Result<(), String> {
+ if is_table_exists(pool, &args.fantoir_table).await {
+ if is_table_empty(&pool, &args.fantoir_table).await {
+ return Ok(());
+ }
+
+ if args.overwrite_table {
+ truncate_table(&pool, &args.fantoir_table).await;
+ return Ok(());
+ }
+
+ return Err(format!(
+ "Table {} already exists and contains rows. To overwrite it, run the import tool with -t option.",
+ &args.fantoir_table
+ ));
+ }
+
+ if args.create_table {
+ create_table(&pool, &args.fantoir_table).await;
+ return Ok(());
+ }
+
+ Err(format!(
+ "Table {} doesn't exist. To create it, run the import tool with -c option.",
+ &args.fantoir_table
+ ))
+}
+
+async fn create_table(pool: &PgPool, table: &str) {
+ let queries = include_str!("../schema/fantoir.sql")
+ .replace("/*table*/fantoir", table)
+ .replace("/*index*/index_fantoir_", format!("index_{}_", table).as_ref());
+
+ run_multiple_queries(pool, &queries).await;
+}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -1,4 +1,5 @@
-//! Commands for the fantoir-datasource tool.
-
-pub(crate) mod import;
-pub(crate) mod promote;
+//! Commands for the fantoir-datasource tool.
+
+pub(crate) mod import;
+pub(crate) mod promote;
+pub(crate) mod query;
diff --git a/src/commands/promote/mod.rs b/src/commands/promote/mod.rs
--- a/src/commands/promote/mod.rs
+++ b/src/commands/promote/mod.rs
@@ -1,27 +1,27 @@
-//! Command to promote a table as the one to use.
-
-use sqlx::PgPool;
-use crate::db::{connect_to_db, run_multiple_queries_groups};
-
-/// Promotes a FANTOIR table as the relevant version to use
-pub async fn promote (fantoir_table: &str, database_url: &str) {
- let pool = connect_to_db(database_url).await;
- let queries_groups = get_queries_groups(&pool, fantoir_table);
-
- run_multiple_queries_groups(&pool, &queries_groups);
-}
-
-/// Determines the groups of queries needed for promotion
-fn get_queries_groups (pool: &PgPool, fantoir_table: &str) -> Vec<String> {
- let mut queries_groups = vec![
- include_str!("../../schema/promote/config.sql"),
- include_str!("../../schema/promote/fantoir_view.sql"),
- ];
-
- queries_groups
- .into_iter()
- .map(|queries| queries
- .replace("/*table*/fantoir", fantoir_table)
- )
- .collect()
-}
+//! Command to promote a table as the one to use.
+
+use sqlx::PgPool;
+use crate::db::{connect_to_db, run_multiple_queries_groups};
+
+/// Promotes a FANTOIR table as the relevant version to use
+pub async fn promote (fantoir_table: &str, database_url: &str) {
+ let pool = connect_to_db(database_url).await;
+ let queries_groups = get_queries_groups(&pool, fantoir_table);
+
+ run_multiple_queries_groups(&pool, &queries_groups);
+}
+
+/// Determines the groups of queries needed for promotion
+fn get_queries_groups (pool: &PgPool, fantoir_table: &str) -> Vec<String> {
+ let mut queries_groups = vec![
+ include_str!("../../schema/promote/config.sql"),
+ include_str!("../../schema/promote/fantoir_view.sql"),
+ ];
+
+ queries_groups
+ .into_iter()
+ .map(|queries| queries
+ .replace("/*table*/fantoir", fantoir_table)
+ )
+ .collect()
+}
diff --git a/src/commands/query.rs b/src/commands/query.rs
new file mode 100644
--- /dev/null
+++ b/src/commands/query.rs
@@ -0,0 +1,68 @@
+use std::process::exit;
+
+use sqlx::PgPool;
+
+use crate::db::connect_to_db;
+use crate::QueryArgs;
+use crate::services::query::*;
+
+static EXIT_CODE_NO_RESULT_FOUND: i32 = 4;
+
+pub async fn search(args: QueryArgs, database_url: &str) {
+ let pool = connect_to_db(database_url).await;
+
+ if args.code_insee.is_some() && args.code_voie.is_some() {
+ let code_fantoir = search_fantoir_code(
+ &pool,
+ &args.code_insee.unwrap(),
+ &args.code_voie.unwrap(),
+ ).await;
+
+ if let Some(code) = code_fantoir {
+ search_one_row(&pool, &code).await;
+ return;
+ }
+
+ exit(EXIT_CODE_NO_RESULT_FOUND);
+ }
+
+ if args.libelle.len() > 0 {
+ search_libelle(&pool, args).await;
+ return;
+ }
+
+ unimplemented!()
+}
+
+async fn search_one_row(pool: &PgPool, code_fantoir: &str) {
+ match query_fantoir_code(pool, code_fantoir).await {
+ None => {
+ exit(EXIT_CODE_NO_RESULT_FOUND);
+ }
+ Some(result) => {
+ println!("{}", result);
+ }
+ }
+}
+
+async fn search_libelle(pool: &PgPool, args: QueryArgs) {
+ let expression = args.libelle.join(" ");
+
+ query_libelle(pool, &expression)
+ .await
+ .iter()
+ .filter(|&entry| entry_matches_conditions(entry, &args))
+ .for_each(|entry| {
+ println!("{}", entry);
+ });
+}
+
+fn entry_matches_conditions(entry: &FantoirVoieResult, conditions: &QueryArgs) -> bool {
+ if let Some(code_insee) = &conditions.code_insee {
+ if &entry.code_insee != code_insee {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/db.rs b/src/db.rs
--- a/src/db.rs
+++ b/src/db.rs
@@ -1,83 +1,83 @@
-//! # Utilities for database.
-//!
-//! This module provides helpers to interact with a PostgreSQL database.
-//! Functions expect to work with an executor from sqlx crate.
-
-use async_scoped::TokioScope;
-use sqlx::PgPool;
-use sqlx::postgres::PgPoolOptions;
-
-static QUERIES_SEPARATOR: &str = "\n\n\n";
-
-pub async fn connect_to_db (database_url: &str) -> PgPool {
- PgPoolOptions::new()
- .max_connections(3)
- .connect(database_url)
- .await
- .expect("Can't connect to database.")
-}
-
-pub async fn is_table_exists (pool: &PgPool, table: &str) -> bool {
- let query = r#"
- SELECT EXISTS (
- SELECT FROM
- pg_tables
- WHERE
- schemaname = 'public' AND
- tablename = $1
- );
- "#;
-
- let result: (bool,) = sqlx::query_as(query)
- .bind(table)
- .fetch_one(pool)
- .await
- .expect("Can't check if table exists.");
-
- result.0
-}
-
-pub async fn is_table_empty (pool: &PgPool, table: &str) -> bool {
- let query = r#"
- SELECT EXISTS (
- SELECT 1 FROM %%table%%
- );
- "#.replace("%%table%%", table);
-
- let result: (bool,) = sqlx::query_as(&query)
- .fetch_one(pool)
- .await
- .expect("Can't check if table is empty.");
-
- !result.0
-}
-
-pub async fn truncate_table (pool: &PgPool, table: &str) {
- let query = format!("TRUNCATE TABLE {} RESTART IDENTITY;", table);
-
- sqlx::query(&query)
- .bind(table)
- .execute(pool)
- .await
- .expect("Can't truncate table.");
-}
-
-pub async fn run_multiple_queries(pool: &PgPool, queries: &str) {
- for query in queries.split(QUERIES_SEPARATOR) {
- sqlx::query(&query)
- .execute(pool)
- .await
- .expect("Can't run SQL query.");
- }
-}
-
-pub fn run_multiple_queries_groups (pool: &PgPool, queries_groups: &Vec<String>) {
- let n = queries_groups.len();
- TokioScope::scope_and_block(|scope| {
- for i in 0..n {
- scope.spawn(
- run_multiple_queries(pool, &queries_groups[i])
- )
- }
- });
-}
+//! # Utilities for database.
+//!
+//! This module provides helpers to interact with a PostgreSQL database.
+//! Functions expect to work with an executor from sqlx crate.
+
+use async_scoped::TokioScope;
+use sqlx::PgPool;
+use sqlx::postgres::PgPoolOptions;
+
+static QUERIES_SEPARATOR: &str = "\n\n\n";
+
+pub async fn connect_to_db (database_url: &str) -> PgPool {
+ PgPoolOptions::new()
+ .max_connections(3)
+ .connect(database_url)
+ .await
+ .expect("Can't connect to database.")
+}
+
+pub async fn is_table_exists (pool: &PgPool, table: &str) -> bool {
+ let query = r#"
+ SELECT EXISTS (
+ SELECT FROM
+ pg_tables
+ WHERE
+ schemaname = 'public' AND
+ tablename = $1
+ );
+ "#;
+
+ let result: (bool,) = sqlx::query_as(query)
+ .bind(table)
+ .fetch_one(pool)
+ .await
+ .expect("Can't check if table exists.");
+
+ result.0
+}
+
+pub async fn is_table_empty (pool: &PgPool, table: &str) -> bool {
+ let query = r#"
+ SELECT EXISTS (
+ SELECT 1 FROM %%table%%
+ );
+ "#.replace("%%table%%", table);
+
+ let result: (bool,) = sqlx::query_as(&query)
+ .fetch_one(pool)
+ .await
+ .expect("Can't check if table is empty.");
+
+ !result.0
+}
+
+pub async fn truncate_table (pool: &PgPool, table: &str) {
+ let query = format!("TRUNCATE TABLE {} RESTART IDENTITY;", table);
+
+ sqlx::query(&query)
+ .bind(table)
+ .execute(pool)
+ .await
+ .expect("Can't truncate table.");
+}
+
+pub async fn run_multiple_queries(pool: &PgPool, queries: &str) {
+ for query in queries.split(QUERIES_SEPARATOR) {
+ sqlx::query(&query)
+ .execute(pool)
+ .await
+ .expect("Can't run SQL query.");
+ }
+}
+
+pub fn run_multiple_queries_groups (pool: &PgPool, queries_groups: &Vec<String>) {
+ let n = queries_groups.len();
+ TokioScope::scope_and_block(|scope| {
+ for i in 0..n {
+ scope.spawn(
+ run_multiple_queries(pool, &queries_groups[i])
+ )
+ }
+ });
+}
diff --git a/src/fantoir.rs b/src/fantoir.rs
--- a/src/fantoir.rs
+++ b/src/fantoir.rs
@@ -1,192 +1,192 @@
-//! # Helper methods for FANTOIR database.
-//!
-//! This module offers a structure for a FANTOIR record, methods to parse the file and export it.
-//! Database functions expect to work with an executor from sqlx crate.
-
-use sqlx::PgPool;
-use sqlx::types::chrono::NaiveDate;
-
-/// A voie in the FANTOIR database
-#[derive(Debug)]
-pub struct FantoirEntry {
- /* Identifiers */
- code_fantoir: String,
-
- /* Part 1 - commune */
- departement: String, // Generally an integer, but INSEE uses 2A and 2B for Corse
- code_commune: i32,
- code_insee: String, // Afa in Corse has 2A001
- type_commune: Option<String>,
- is_pseudo_recensee: bool,
-
- /* Part 2 - voie */
- identifiant_communal_voie: String,
- cle_rivoli: String,
- code_nature_voie: Option<String>,
- libelle_voie: String,
- type_voie: i32, // 1: voie, 2: ens. immo, 3: lieu-dit, 4: pseudo-voie, 5: provisoire
- is_public: bool,
-
- /* Part 3 - population */
- is_large: bool,
- population_a_part: i32,
- population_fictive: i32,
-
- /* Part 4 - metadata */
- is_cancelled: bool,
- cancel_date: Option<NaiveDate>,
- creation_date: Option<NaiveDate>,
- code_majic: i32,
- last_alpha_word: String,
-}
-
-impl FantoirEntry {
- pub fn parse_line(line: &str) -> Self {
- let departement = match &line[0..2] {
- "97" => String::from(&line[0..3]), // include for DOM/TOM the next digit
- department => String::from(department),
- };
- let len = line.len();
-
- Self {
- /* Identifier */
- code_fantoir: String::from(&line[0..11]),
-
- /* Part 1 - commune */
- departement,
- code_commune: line[3..6].parse().expect("Can't parse code commune"),
- code_insee: format!("{:02}{:03}", &line[0..2], &line[3..6]),
- type_commune: parse_optional_string(&line[43..44]),
- is_pseudo_recensee: &line[45..46] == "3",
-
- /* Part 2 - voie */
- identifiant_communal_voie: String::from(&line[6..10]),
- cle_rivoli: String::from(&line[10..11]),
- code_nature_voie: parse_optional_string(&line[11..15]),
- libelle_voie: String::from(line[15..41].trim()),
- type_voie: line[108..109].parse().expect("Can't parse type de voie."),
- is_public: &line[48..49] == "0",
-
- /* Part 3 - population */
- is_large: &line[49..50] == "*",
- population_a_part: line[59..66].parse().expect("Can't parse population à part"),
- population_fictive: line[66..73].parse().expect("Can't parse population fictive"),
-
- /* Part 4 - metadata */
- is_cancelled: &line[73..74] != " ",
- cancel_date: parse_fantoir_date(&line[74..81]),
- creation_date: parse_fantoir_date(&line[81..88]),
- code_majic: line[103..108].parse().expect("Can't parse MAJIC"),
- last_alpha_word: String::from(&line[112..len]),
- }
- }
-
- pub async fn insert_to_db(&self, pool: &PgPool, table: &str) {
- let mut query = format!("INSERT INTO {}", table);
- query.push_str(
- r#"
- (code_fantoir,
- departement, code_commune, code_insee, type_commune, is_pseudo_recensee,
- identifiant_communal_voie, cle_rivoli, code_nature_voie, libelle_voie, type_voie, is_public,
- is_large, population_a_part, population_fictive,
- is_cancelled, cancel_date, creation_date, code_majic, last_alpha_word
- )
- VALUES
- ($1,
- $2, $3, $4, $5, $6,
- $7, $8, $9, $10, $11, $12,
- $13, $14, $15,
- $16, $17, $18, $19, $20
- )"#
- );
-
- sqlx::query(&query)
- /* Identifiers */
- .bind(&self.code_fantoir)
-
- /* Part 1 - commune */
- .bind(&self.departement)
- .bind(&self.code_commune)
- .bind(&self.code_insee)
- .bind(&self.type_commune)
- .bind(&self.is_pseudo_recensee)
-
- /* Part 2 - Voie */
- .bind(&self.identifiant_communal_voie)
- .bind(&self.cle_rivoli)
- .bind(&self.code_nature_voie)
- .bind(&self.libelle_voie)
- .bind(&self.type_voie)
- .bind(&self.is_public)
-
- /* Part 3 - Population */
- .bind(&self.is_large)
- .bind(&self.population_a_part)
- .bind(&self.population_fictive)
-
- /* Part 4 - Metadata */
- .bind(&self.is_cancelled)
- .bind(&self.cancel_date)
- .bind(&self.creation_date)
- .bind(&self.code_majic)
- .bind(&self.last_alpha_word)
-
- .execute(pool)
- .await
- .expect("Can't insert entry to database");
- }
-}
-
-pub fn parse_fantoir_date (date: &str) -> Option<NaiveDate> {
- if date == "0000000" {
- return None;
- }
-
- let year = date[0..4].parse().expect("Can't parse date: year part");
- let ord = date[4..7].parse().expect("Can't parse date: ordinal part");
-
- NaiveDate::from_yo_opt(year, ord)
-}
-
-fn parse_optional_string (expression: &str) -> Option<String> {
- let expression = expression.trim();
-
- if expression.len() > 0 {
- Some(String::from(expression))
- } else {
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- // Note this useful idiom: importing names from outer (for mod tests) scope.
- use super::*;
-
- #[test]
- fn test_parse_fantoir_date() {
- let expected = NaiveDate::from_ymd_opt(1987, 1, 1).unwrap();
- let actual = parse_fantoir_date("1987001");
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn test_parse_optional_string() {
- assert_eq!(Some(String::from("quux")), parse_optional_string("quux"));
- }
-
- #[test]
- fn test_parse_optional_string_with_trailing_spaces() {
- assert_eq!(Some(String::from("quux")), parse_optional_string("quux "));
- }
-
- #[test]
- fn test_parse_optional_string_when_empty() {
- assert_eq!(true, parse_optional_string("").is_none());
- }
-
- #[test]
- fn test_parse_optional_string_when_only_spaces() {
- assert_eq!(true, parse_optional_string(" ").is_none());
- }
-}
+//! # Helper methods for FANTOIR database.
+//!
+//! This module offers a structure for a FANTOIR record, methods to parse the file and export it.
+//! Database functions expect to work with an executor from sqlx crate.
+
+use sqlx::PgPool;
+use sqlx::types::chrono::NaiveDate;
+
+/// A voie in the FANTOIR database
+#[derive(Debug)]
+pub struct FantoirEntry {
+ /* Identifiers */
+ code_fantoir: String,
+
+ /* Part 1 - commune */
+ departement: String, // Generally an integer, but INSEE uses 2A and 2B for Corse
+ code_commune: i32,
+ code_insee: String, // Afa in Corse has 2A001
+ type_commune: Option<String>,
+ is_pseudo_recensee: bool,
+
+ /* Part 2 - voie */
+ identifiant_communal_voie: String,
+ cle_rivoli: String,
+ code_nature_voie: Option<String>,
+ libelle_voie: String,
+ type_voie: i32, // 1: voie, 2: ens. immo, 3: lieu-dit, 4: pseudo-voie, 5: provisoire
+ is_public: bool,
+
+ /* Part 3 - population */
+ is_large: bool,
+ population_a_part: i32,
+ population_fictive: i32,
+
+ /* Part 4 - metadata */
+ is_cancelled: bool,
+ cancel_date: Option<NaiveDate>,
+ creation_date: Option<NaiveDate>,
+ code_majic: i32,
+ last_alpha_word: String,
+}
+
+impl FantoirEntry {
+ pub fn parse_line(line: &str) -> Self {
+ let departement = match &line[0..2] {
+ "97" => String::from(&line[0..3]), // include for DOM/TOM the next digit
+ department => String::from(department),
+ };
+ let len = line.len();
+
+ Self {
+ /* Identifier */
+ code_fantoir: String::from(&line[0..11]),
+
+ /* Part 1 - commune */
+ departement,
+ code_commune: line[3..6].parse().expect("Can't parse code commune"),
+ code_insee: format!("{:02}{:03}", &line[0..2], &line[3..6]),
+ type_commune: parse_optional_string(&line[43..44]),
+ is_pseudo_recensee: &line[45..46] == "3",
+
+ /* Part 2 - voie */
+ identifiant_communal_voie: String::from(&line[6..10]),
+ cle_rivoli: String::from(&line[10..11]),
+ code_nature_voie: parse_optional_string(&line[11..15]),
+ libelle_voie: String::from(line[15..41].trim()),
+ type_voie: line[108..109].parse().expect("Can't parse type de voie."),
+ is_public: &line[48..49] == "0",
+
+ /* Part 3 - population */
+ is_large: &line[49..50] == "*",
+ population_a_part: line[59..66].parse().expect("Can't parse population à part"),
+ population_fictive: line[66..73].parse().expect("Can't parse population fictive"),
+
+ /* Part 4 - metadata */
+ is_cancelled: &line[73..74] != " ",
+ cancel_date: parse_fantoir_date(&line[74..81]),
+ creation_date: parse_fantoir_date(&line[81..88]),
+ code_majic: line[103..108].parse().expect("Can't parse MAJIC"),
+ last_alpha_word: String::from(&line[112..len]),
+ }
+ }
+
+ pub async fn insert_to_db(&self, pool: &PgPool, table: &str) {
+ let mut query = format!("INSERT INTO {}", table);
+ query.push_str(
+ r#"
+ (code_fantoir,
+ departement, code_commune, code_insee, type_commune, is_pseudo_recensee,
+ identifiant_communal_voie, cle_rivoli, code_nature_voie, libelle_voie, type_voie, is_public,
+ is_large, population_a_part, population_fictive,
+ is_cancelled, cancel_date, creation_date, code_majic, last_alpha_word
+ )
+ VALUES
+ ($1,
+ $2, $3, $4, $5, $6,
+ $7, $8, $9, $10, $11, $12,
+ $13, $14, $15,
+ $16, $17, $18, $19, $20
+ )"#
+ );
+
+ sqlx::query(&query)
+ /* Identifiers */
+ .bind(&self.code_fantoir)
+
+ /* Part 1 - commune */
+ .bind(&self.departement)
+ .bind(&self.code_commune)
+ .bind(&self.code_insee)
+ .bind(&self.type_commune)
+ .bind(&self.is_pseudo_recensee)
+
+ /* Part 2 - Voie */
+ .bind(&self.identifiant_communal_voie)
+ .bind(&self.cle_rivoli)
+ .bind(&self.code_nature_voie)
+ .bind(&self.libelle_voie)
+ .bind(&self.type_voie)
+ .bind(&self.is_public)
+
+ /* Part 3 - Population */
+ .bind(&self.is_large)
+ .bind(&self.population_a_part)
+ .bind(&self.population_fictive)
+
+ /* Part 4 - Metadata */
+ .bind(&self.is_cancelled)
+ .bind(&self.cancel_date)
+ .bind(&self.creation_date)
+ .bind(&self.code_majic)
+ .bind(&self.last_alpha_word)
+
+ .execute(pool)
+ .await
+ .expect("Can't insert entry to database");
+ }
+}
+
+pub fn parse_fantoir_date (date: &str) -> Option<NaiveDate> {
+ if date == "0000000" {
+ return None;
+ }
+
+ let year = date[0..4].parse().expect("Can't parse date: year part");
+ let ord = date[4..7].parse().expect("Can't parse date: ordinal part");
+
+ NaiveDate::from_yo_opt(year, ord)
+}
+
+fn parse_optional_string (expression: &str) -> Option<String> {
+ let expression = expression.trim();
+
+ if expression.len() > 0 {
+ Some(String::from(expression))
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ // Note this useful idiom: importing names from outer (for mod tests) scope.
+ use super::*;
+
+ #[test]
+ fn test_parse_fantoir_date() {
+ let expected = NaiveDate::from_ymd_opt(1987, 1, 1).unwrap();
+ let actual = parse_fantoir_date("1987001");
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_parse_optional_string() {
+ assert_eq!(Some(String::from("quux")), parse_optional_string("quux"));
+ }
+
+ #[test]
+ fn test_parse_optional_string_with_trailing_spaces() {
+ assert_eq!(Some(String::from("quux")), parse_optional_string("quux "));
+ }
+
+ #[test]
+ fn test_parse_optional_string_when_empty() {
+ assert_eq!(true, parse_optional_string("").is_none());
+ }
+
+ #[test]
+ fn test_parse_optional_string_when_only_spaces() {
+ assert_eq!(true, parse_optional_string(" ").is_none());
+ }
+}
diff --git a/src/main.rs b/src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,12 +2,12 @@
use clap::{Args, Parser};
-use crate::commands::import::import;
use crate::commands::promote::promote;
mod commands;
mod db;
mod fantoir;
+mod services;
#[derive(Debug, Parser)]
#[command(name = "fantoir-datasource")]
@@ -20,6 +20,10 @@
/// Promote an imported FANTOIR table as the current FANTOIR table to use
#[command(arg_required_else_help = true)]
Promote(PromoteArgs),
+
+ /// Query the imported FANTOIR table
+ #[command(arg_required_else_help = true)]
+ Query(QueryArgs)
}
#[derive(Debug, Args)]
@@ -46,6 +50,21 @@
fantoir_table: String,
}
+#[derive(Debug, Args)]
+#[clap(trailing_var_arg=true)]
+pub struct QueryArgs {
+ /// INSEE code to identify a commune
+ #[arg(long)]
+ code_insee: Option<String>,
+
+ /// Identifier of the voie by the commune
+ #[arg(long)]
+ code_voie: Option<String>,
+
+ /// Expression to search
+ libelle: Vec<String>,
+}
+
#[tokio::main]
async fn main() {
let command = FantoirCommand::parse(); // Will exit if argument is missing or --help/--version provided.
@@ -55,10 +74,13 @@
match command {
FantoirCommand::Import(args) => {
- import(&args, &database_url).await;
+ commands::import::import(&args, &database_url).await;
},
FantoirCommand::Promote(args) => {
promote(&args.fantoir_table, &database_url).await;
},
+ FantoirCommand::Query(args) => {
+ commands::query::search(args, &database_url).await
+ },
};
}
diff --git a/src/services/mod.rs b/src/services/mod.rs
new file mode 100644
--- /dev/null
+++ b/src/services/mod.rs
@@ -0,0 +1 @@
+pub mod query;
diff --git a/src/services/query.rs b/src/services/query.rs
new file mode 100644
--- /dev/null
+++ b/src/services/query.rs
@@ -0,0 +1,112 @@
+//! Service to search imported FANTOIR table
+//! This is intended to be exposed to the tool, and used internally to fix FANTOIR codes.
+
+use std::fmt::{Display, Formatter};
+use sqlx::{Error, FromRow, PgPool};
+
+/* -------------------------------------------------------------
+ Search a fantoir code from INSEE code, identifiant communal.
+
+ Useful to fix fantoir code from other sources.
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
+
+pub async fn search_fantoir_code(pool: &PgPool, code_insee: &str, identifiant_communal_voie: &str) -> Option<String> {
+ let result = sqlx::query!(r#"
+SELECT code_fantoir
+FROM fantoir
+WHERE code_insee = $1 AND identifiant_communal_voie = $2
+ "#, code_insee, identifiant_communal_voie)
+ .fetch_one(pool)
+ .await;
+
+ if let Err(Error::RowNotFound) = result {
+ return None;
+ }
+
+ result.unwrap().code_fantoir
+}
+
+/* -------------------------------------------------------------
+ Query short information about voies.
+
+ This tool is mainly intended as an import tool, but as we need
+ this query service to cross datasources, we can leverage this
+ to offer a small search facility.
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
+
+#[derive(Debug, Clone, sqlx::FromRow)]
+pub struct FantoirVoieResult {
+ pub code_fantoir: String,
+ pub code_insee: String,
+ pub identifiant_communal_voie: String,
+ pub code_nature_voie: Option<String>,
+ pub libelle_voie: String,
+}
+
+impl FantoirVoieResult {
+ fn get_name (&self) -> String {
+ match &self.code_nature_voie {
+ None => self.libelle_voie.to_string(),
+ Some(kind) => match kind.len() {
+ 0 => self.libelle_voie.to_string(),
+ _ => format!("{} {}", kind, self.libelle_voie)
+ }
+ }
+ }
+}
+
+impl Display for FantoirVoieResult {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ write!(
+ f, "{}\t{} {}\t{}",
+ self.code_fantoir, self.code_insee, self.identifiant_communal_voie, self.get_name()
+ )
+ }
+}
+
+pub async fn query_fantoir_code(pool: &PgPool, code_fantoir: &str) -> Option<FantoirVoieResult> {
+ let result = sqlx::query!(r#"
+SELECT code_insee, identifiant_communal_voie, code_nature_voie, libelle_voie
+FROM fantoir
+WHERE code_fantoir = $1;
+ "#, code_fantoir)
+ .fetch_one(pool)
+ .await;
+
+ if let Err(Error::RowNotFound) = result {
+ return None;
+ }
+
+ let result = result.unwrap();
+
+ Some(
+ FantoirVoieResult {
+ code_fantoir: code_fantoir.to_string(),
+ code_insee: result.code_insee.unwrap(),
+ identifiant_communal_voie: result.identifiant_communal_voie.unwrap(),
+ code_nature_voie: result.code_nature_voie,
+ libelle_voie: result.libelle_voie.unwrap(),
+ }
+ )
+}
+
+pub async fn query_libelle (pool: &PgPool, libelle: &str) -> Vec<FantoirVoieResult> {
+ let result = sqlx::query(r#"
+SELECT code_fantoir, code_insee, identifiant_communal_voie, code_nature_voie, libelle_voie
+FROM fantoir
+WHERE libelle_voie ILIKE CONCAT('%', $1, '%');
+ "#)
+ .bind(libelle)
+ .fetch_all(pool)
+ .await;
+
+ if let Err(Error::RowNotFound) = result {
+ return Vec::new();
+ }
+
+ result
+ .unwrap()
+ .iter()
+ .map(|row| FantoirVoieResult::from_row(row).unwrap())
+ .collect()
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Nov 18, 15:03 (4 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2250620
Default Alt Text
D2734.id6934.diff (33 KB)
Attached To
Mode
D2734: Search the imported FANTOIR database
Attached
Detach File
Event Timeline
Log In to Comment