Skip to content
This repository was archived by the owner on Aug 18, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c9a3d5f
Remove old tests for now
djc Nov 11, 2023
917d3f1
Remove old CLI for now
djc Nov 11, 2023
d190cf6
Improve todo!() messages
djc Nov 11, 2023
45f7918
Clean up StructBuilder construction
djc Nov 11, 2023
0fe17e6
Add example to generate types from database
djc Nov 11, 2023
b1413ff
Clean up imports
djc Nov 11, 2023
bd901ac
Derive Default impl for StructBuilder
djc Nov 11, 2023
e64107a
Rename StructBuilder to Table
djc Nov 11, 2023
1070e8c
Remove trivial setters for public fields
djc Nov 11, 2023
9ca67ac
Import fmt from std
djc Nov 11, 2023
bde9f35
Combine match arms
djc Nov 11, 2023
3466bb6
Move inherent impl before trait impls
djc Nov 11, 2023
232affe
Simplify Display impl for Table
djc Nov 11, 2023
b10ae70
Move ForeignKey type definition down
djc Nov 11, 2023
44aa634
Remove trivial methods
djc Nov 11, 2023
04cc80d
Stop deriving Default for Table
djc Nov 11, 2023
21d3839
Use Arc<str> instead of Cow<str>
djc Nov 11, 2023
25fe98f
Add basic formatting test
djc Nov 11, 2023
b670f23
Add creation types to generated code
djc Nov 11, 2023
e20d2c8
Lowercase panic message
djc Nov 11, 2023
26fb35a
Integrate insert() method setup
djc Nov 11, 2023
596ea79
Remove unused code
djc Nov 11, 2023
0ac0828
More precise panic message for unknown types
djc Nov 11, 2023
71a493d
Deduplicate lifetime formatting
djc Nov 11, 2023
2d019bf
Use tuple variants for Type
djc Nov 11, 2023
0a1da0d
Import variants to cut down on noise
djc Nov 11, 2023
4266cd4
Support for ingesting Postgres enum types
djc Nov 11, 2023
cb37151
Rename Type::from_postgres() to from_postgres_by_name()
djc Nov 11, 2023
ebce7b2
Add basic support for arrays
djc Nov 12, 2023
101643c
Improve error handling for wrong number of types found
djc Nov 13, 2023
5a7eadc
Add some support for extracting ForeignKey data
djc Nov 13, 2023
ec1ce7b
Add support for arrays of primitives
djc Nov 13, 2023
633b40c
Add int4 alias for integer
djc Nov 13, 2023
78997db
Sort match arms in alphabetic order
djc Nov 13, 2023
c0368a9
Add support for JSONB
djc Nov 13, 2023
cb6c4f2
Try directly matching built-in OIDs
djc Nov 13, 2023
5c5a7bb
Integrate support for composite types
djc Nov 13, 2023
64b4a88
Add more built-in scalar types
djc Nov 13, 2023
724e37d
Ignore CHECK constraints for now
djc Nov 13, 2023
eb5e366
Add support for pgvector types
djc Nov 13, 2023
153f443
Add support for a few more built-in types
djc Nov 13, 2023
e1afc3a
Extend Display implementations
djc Nov 14, 2023
95cb7b6
Include column type definitions in schema
djc Nov 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,22 @@ edition = "2021"
rust-version = "1.70"
license = "Apache-2.0 OR MIT"

[[bin]]
name = "cli"
required-features = ["postgres", "clap"]
[features]
default = ["postgres"]
postgres = ["tokio-postgres"]

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"], optional = true }
async-recursion = "1"
heck = "0.4.0"
indexmap = "2"
postgres = { version = "0.19.3", optional = true }
postgres-types = { version = "0.2.3", features = ["derive"] }
thiserror = "1.0.31"
time = { version = "0.3.9", features = ["parsing"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"], optional = true }

[dev-dependencies]
chrono = "0.4"
postgres = { version = "0.19.3", features = ["with-chrono-0_4"] }
clap = { version = "4", features = ["derive"] }
similar-asserts = "1.4.2"
tempfile = "3.3"

[features]
default = ["postgres"]
22 changes: 22 additions & 0 deletions examples/generate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use clap::Parser;
use instant_models::Schema;
use tokio_postgres::NoTls;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let opts = Options::parse();
let (client, conn) = tokio_postgres::connect(&opts.db, NoTls).await?;
tokio::spawn(conn);

let schema = Schema::from_postgres(&client).await?;
println!("{schema}");

Ok(())
}

/// Generate Rust code from Postgres database
#[derive(Parser, Debug)]
struct Options {
/// Database to inspect (as URL)
db: String,
}
48 changes: 0 additions & 48 deletions src/bin/cli.rs

This file was deleted.

110 changes: 59 additions & 51 deletions src/column.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
use crate::{Type, TypeAsRef};
use std::fmt;
use std::sync::Arc;

use heck::AsSnakeCase;
use std::borrow::Cow;
use tokio_postgres::Client;

#[derive(Debug, PartialEq)]
pub struct ForeignKey {
to_table: Cow<'static, str>,
columns: Vec<Cow<'static, str>>,
}
use crate::types::{Type, TypeAsRef};

#[derive(Debug, PartialEq)]
pub struct Column {
pub name: Cow<'static, str>,
pub name: Arc<str>,
pub r#type: Type,
pub null: bool,
pub primary_key: bool,
pub foreign_key: Option<ForeignKey>,
pub unique: bool,
pub default: Option<Cow<'static, str>>,
pub type_def: Option<Cow<'static, str>>,
pub default: Option<String>,
pub type_def: Option<String>,
}

impl Column {
pub fn new(name: Cow<'static, str>, r#type: Type) -> Self {
pub fn new(name: Arc<str>, r#type: Type) -> Self {
Self {
name,
r#type,
Expand All @@ -34,39 +32,16 @@ impl Column {
}
}

pub fn set_null(mut self, value: bool) -> Self {
self.null = value;
self
}

pub fn set_primary_key(mut self, value: bool) -> Self {
self.primary_key = value;
self
}

pub fn set_foreign_key(mut self, value: Option<ForeignKey>) -> Self {
self.foreign_key = value;
self
}

pub fn set_unique(mut self, value: bool) -> Self {
self.unique = value;
self
}

pub fn set_default(mut self, value: Option<Cow<'static, str>>) -> Self {
self.default = value;
self
}

pub fn set_type_def(mut self, value: Option<Cow<'static, str>>) -> Self {
self.type_def = value;
self
pub fn new_field(&self) -> NewField {
NewField {
lifetime: Some("a"),
val: self,
}
}
}

impl std::fmt::Display for Column {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
impl fmt::Display for Column {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.null {
write!(fmt, "{}: Option<{}>", AsSnakeCase(&self.name), self.r#type)
} else {
Expand All @@ -75,16 +50,16 @@ impl std::fmt::Display for Column {
}
}

pub struct NewValue<'a> {
pub struct NewField<'a> {
pub lifetime: Option<&'a str>,
pub val: &'a Column,
}

impl std::fmt::Display for NewValue<'_> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
impl fmt::Display for NewField<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.val.null && self.val.default.is_some() {
panic!(
"Column `{}` is both NULL and takes a default value `{}`",
"column `{}` is both NULL and takes a default value `{}`",
self.val.name,
self.val.default.as_ref().unwrap()
);
Expand Down Expand Up @@ -116,13 +91,46 @@ impl std::fmt::Display for NewValue<'_> {
#[derive(Debug, PartialEq)]
pub enum Constraint {
ForeignKey {
name: Cow<'static, str>,
columns: Cow<'static, [Cow<'static, str>]>,
ref_table: Cow<'static, str>,
ref_columns: Cow<'static, [Cow<'static, str>]>,
name: String,
columns: Vec<String>,
ref_table: String,
ref_columns: Vec<String>,
},
PrimaryKey {
name: Cow<'static, str>,
columns: Vec<Cow<'static, str>>,
name: Vec<String>,
columns: Vec<String>,
},
}

#[derive(Debug, PartialEq)]
pub struct ForeignKey {
to_table: String,
columns: Vec<String>,
}

impl ForeignKey {
pub(crate) async fn from_postgres(
name: &str,
client: &Client,
) -> Result<Self, tokio_postgres::Error> {
let sql = r#"
SELECT tc.table_name, kcu.column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.constraint_name = $1
"#;

let rows = client.query(sql, &[&name]).await?;
let row = match &rows[..] {
[row] => row,
[] => panic!("foreign key constraint {name} not found"),
_ => todo!("unsupported composite foreign key {name}"),
};

Ok(Self {
to_table: row.get::<_, &str>(0).to_owned(),
columns: vec![row.get::<_, &str>(1).to_owned()],
})
}
}
66 changes: 61 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,64 @@
mod struct_builder;
pub use struct_builder::*;
use std::fmt;

mod column;
pub use column::*;
use tokio_postgres::Client;

mod table;
pub use table::Table;
mod column;
pub use column::Column;
mod types;
pub use types::*;
pub use types::{Enum, Type, TypeDefinition, Composite};

pub struct Schema {
pub tables: Vec<Table>,
pub types: Vec<TypeDefinition>,
}

impl Schema {
#[cfg(feature = "postgres")]
pub async fn from_postgres(client: &Client) -> anyhow::Result<Self> {
let sql = r#"
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
"#;

let (mut tables, mut types) = (Vec::new(), Vec::new());
for row in client.query(sql, &[]).await? {
let name = row.get::<_, &str>(0);
let table = Table::from_postgres(name, client).await?;

for column in table.columns.values() {
match &column.r#type {
Type::Composite(c) => types.push(TypeDefinition::Composite(
Composite::from_postgres(&c.name, client).await?,
)),
Type::Enum(e) => types.push(TypeDefinition::Enum(
Enum::from_postgres(&e.name, client).await?,
)),
_ => {}
}
}

tables.push(table);
}

Ok(Self { tables, types })
}
}

impl fmt::Display for Schema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for table in &self.tables {
f.write_fmt(format_args!("{table}"))?;
f.write_str("\n")?;
}

for type_def in &self.types {
f.write_fmt(format_args!("{type_def}"))?;
f.write_str("\n")?;
}

Ok(())
}
}
Loading