Page MenuHomeDevCentral

D2734.id6934.diff
No OneTemporary

D2734.id6934.diff

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

Mime Type
text/plain
Expires
Mon, Nov 18, 11:40 (56 m, 51 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2250620
Default Alt Text
D2734.id6934.diff (33 KB)

Event Timeline