Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F11723404
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
10 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93c33a2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target
+FANTOIR*
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0ab04e3
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "fantoir2db"
+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.clap]
+version = "~4.0.32"
+features = ["derive"]
+
+[dependencies.sqlx]
+version = "~0.6.2"
+features = ["runtime-tokio-native-tls", "postgres"]
+
+[dependencies.tokio]
+version = "~1.23.0"
+features = ["full"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7a1b8e0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+## How to use?
+
+Define your PostgreSQL connection URL in environment:
+
+```
+export DATABASE_URL="postgres://fantoir:fantoir@localhost/fantoir"
+```
+
+## Development
+
+### Build instructions
+
+The PostgreSQL library is required to link against it.
+If not found, you can add the path to the LIB environment variable.
+
+### Prepare a test database
+
+Execute the following queries as postgres user:
+
+```
+CREATE ROLE fantoir WITH PASSWORD 'fantoir' LOGIN;
+CREATE DATABASE fantoir OWNER fantoir;
+```
+
+You can then use the code with the default DATABASE_URL documented above.
diff --git a/src/db.rs b/src/db.rs
new file mode 100644
index 0000000..1f85743
--- /dev/null
+++ b/src/db.rs
@@ -0,0 +1,60 @@
+//! # Utilities for database.
+//!
+//! This module provides helpers to interact with a PostgreSQL database.
+//! Functions expect to work with an executor from sqlx crate.
+
+use sqlx::PgPool;
+use sqlx::postgres::PgPoolOptions;
+
+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 {};", table);
+
+ sqlx::query(&query)
+ .bind(table)
+ .execute(pool)
+ .await
+ .expect("Can't truncate table.");
+}
diff --git a/src/fantoir.rs b/src/fantoir.rs
new file mode 100644
index 0000000..cb60a5d
--- /dev/null
+++ b/src/fantoir.rs
@@ -0,0 +1,104 @@
+//! # 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;
+
+/// A voie in the FANTOIR database
+#[derive(Debug)]
+pub struct FantoirEntry {
+ /* Part 1 - commune */
+ departement: i32,
+ code_commune: i32,
+ code_insee: i32,
+ type_commune: i32,
+
+ /* Part 2 - voie */
+
+ /* Part 3 - population */
+
+ /* Part 4 - metadata */
+
+}
+
+impl FantoirEntry {
+ pub fn parse_line(line: &str) -> Self {
+ let departement = match &line[0..2] {
+ "97" => line[0..2].parse().expect("Can't parse DOM/TOM department"),
+ department => department.parse().expect("Can't parse department"),
+ };
+ let code_insee = format!("{:02}{:03}", &line[0..2], &line[3..6])
+ .parse()
+ .expect("Can't parse INSEE code");
+
+ Self {
+ /* Part 1 - commune */
+ departement,
+ code_commune: line[3..6].parse().expect("Can't parse code commune"),
+ code_insee,
+ type_commune: 0,
+
+ /* Part 2 - voie */
+
+ /* Part 3 - population */
+
+ /* Part 4 - metadata */
+
+ }
+ }
+
+ pub async fn insert_to_db(&self, pool: &PgPool, table: &str) {
+ let mut query = format!("INSERT INTO {}", table);
+ query.push_str(
+ r#"
+ (departement, code_commune, code_insee, type_commune)
+ VALUES
+ ($1, $2, $3, $4)"#
+ );
+
+ sqlx::query(&query)
+
+ /* Part 1 - commune */
+ .bind(&self.departement)
+ .bind(&self.code_commune)
+ .bind(&self.code_insee)
+ .bind(&self.type_commune)
+
+ .execute(pool)
+ .await
+ .expect("Can't insert entry to database");
+ }
+}
+
+/*
+ Python code from Nasqueron DB :: voies still to migrate
+
+ voie = {
+ "voie": {
+ "identifiant_communal_voie": line[6:10],
+ "cle_rivoli": line[10:11],
+ "code_nature_voie": code_nature_voie,
+ "nature_voie": self.get_nature_voie(code_nature_voie),
+ "libelle_voie": line[15:41].strip(),
+
+ "RUR": "pseudo-recensée" if line[45:46] == "3" else "",
+
+ "type": type,
+ "type_description": self.get_type_name(type),
+ "public": line[48:49] == "0",
+ },
+ "population": {
+ "is_large": line[49:50] == "*",
+ "à part": int(line[59:66]),
+ "fictive": int(line[66:73]),
+ },
+ "metadata": {
+ "cancelled": line[73:74] != " ",
+ "cancel_date": line[74:81],
+ "creation_date": line[81:88],
+ "MAJIC": line[103:108],
+ "last_alpha_word": line[112:120],
+ }
+ }
+ */
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..ab89a60
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,112 @@
+use std::env;
+use std::process::exit;
+
+use clap::Parser;
+use sqlx::PgPool;
+use tokio::fs::File;
+use tokio::io::{AsyncBufReadExt, BufReader};
+
+use crate::db::*;
+use crate::fantoir::FantoirEntry;
+
+mod db;
+mod fantoir;
+
+#[derive(Parser,Default,Debug)]
+#[clap(author="Nasqueron project", version, about="Import FANTOIR database into PostgreSQL")]
+struct Arguments {
+ /// Create table if it doesn't exist
+ #[arg(short = 'c')]
+ create_table: bool,
+
+ /// Truncate table if it already exists, allowing the overwrite mode.
+ /// If not specified, the script will fail if table exists.
+ /// If the table is truncated, the id primary key field will start to a greater value than 1.
+ #[arg(short = 't')]
+ overwrite_table: bool,
+
+ /// The FANTOIR file to import
+ fantoir_file: String,
+
+ /// The name of the table to populate
+ fantoir_table: String,
+}
+
+#[tokio::main]
+async fn main() {
+ let args = Arguments::parse(); // Will exit if argument is missing or --help/--version provided.
+
+ let database_url = env::var("DATABASE_URL")
+ .expect("The environment variable DATABASE_URL need to be set to your PostgreSQL database.");
+
+ import(&args, &database_url).await;
+}
+
+async fn import(args: &Arguments, 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;
+ }
+
+ let entry = FantoirEntry::parse_line(&line);
+ println!("{:?}", entry);
+ entry.insert_to_db(&pool, &args.fantoir_table).await
+ }
+}
+
+async fn initialize_table(args: &Arguments, 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 query = include_str!("schema.sql")
+ .replace("/*table*/fantoir", table);
+
+ println!("{}", &query);
+ sqlx::query(&query)
+ .execute(pool)
+ .await
+ .expect("Can't create fantoir table.");
+}
diff --git a/src/schema.sql b/src/schema.sql
new file mode 100644
index 0000000..964be4a
--- /dev/null
+++ b/src/schema.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS /*table*/fantoir (
+ id bigserial,
+
+ -- Part 1 - commune
+ departement integer,
+ code_commune integer,
+ code_insee integer,
+ type_commune integer
+);
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Sep 18, 10:17 (12 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2987990
Default Alt Text
(10 KB)
Attached To
Mode
rDS Nasqueron Datasources
Attached
Detach File
Event Timeline
Log In to Comment