Skip to content

Commit 4feb437

Browse files
committed
feat(gb-9009): views support
1 parent 23de776 commit 4feb437

37 files changed

Lines changed: 10869 additions & 221 deletions

Cargo.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/postgres/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "grafbase-postgres"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2024"
55
license = "Apache-2.0"
66

@@ -27,3 +27,5 @@ sqlx = { workspace = true, features = [
2727
] }
2828
semver = { version = "1.0.26", features = ["serde"] }
2929
url = { version = "2.5.4", features = ["serde"] }
30+
toml.workspace = true
31+
dotenv = "0.15.0"

cli/postgres/README.md

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ cargo install --path .
2323

2424
- `DATABASE_URL` - Connection string to your PostgreSQL database
2525

26+
If the current directory has a `.env` file stored with the `DATABASE_URL` environment variable, it will be used as the default value for the `--database-url` option.
27+
2628
### Basic Command
2729

2830
```bash
29-
grafbase-postgres --database-url "postgres://username:password@localhost:5432/mydatabase" introspect --extension-version 1.0.0 --database-name mydb
31+
grafbase-postgres --database-url "postgres://username:password@localhost:5432/mydatabase" introspect --config grafbase-postgres.toml
3032
```
3133

3234
### Command Options
@@ -37,39 +39,34 @@ grafbase-postgres --database-url "postgres://username:password@localhost:5432/my
3739

3840
#### Introspect Command
3941

40-
- `-o, --output-file <PATH>` - Write the SDL output to a file instead of stdout
41-
- `-d, --database-name <NAME>` - Name for the database in the GraphQL SDL (default: "default")
42-
- `-s, --default-schema <SCHEMA>` - Default schema to use (will be omitted from definitions) (default: "public")
43-
- `-u, --extension-url <URL>` - URL to the Grafbase PostgreSQL extension
44-
- `-v, --extension-version <VERSION>` - Version of the Grafbase PostgreSQL extension (semver)
45-
46-
**Note**: Either `--extension-url` or `--extension-version` must be provided.
42+
- `-c, --config <PATH>` - Specify configuration file for introspection. Defaults to `./grafbase-postgres.toml` if not provided.
4743

4844
## Examples
4945

5046
### Output SDL to Terminal
5147

5248
```bash
53-
grafbase-postgres --database-url "postgres://user:pass@localhost:5432/mydb" introspect --extension-version 1.0.0
49+
grafbase-postgres \
50+
--database-url "postgres://user:pass@localhost:5432/mydb" \
51+
introspect \
52+
--config grafbase-postgres.toml
5453
```
5554

5655
### Save SDL to a File
5756

5857
```bash
59-
grafbase-postgres --database-url "postgres://user:pass@localhost:5432/mydb" introspect --extension-version 1.0.0 --output-file schema.graphql
60-
```
61-
62-
### Use a Custom Database Name and Schema
63-
64-
```bash
65-
grafbase-postgres --database-url "postgres://user:pass@localhost:5432/mydb" introspect --extension-version 1.0.0 --database-name production --default-schema app
58+
grafbase-postgres \
59+
--database-url "postgres://user:pass@localhost:5432/mydb" \
60+
introspect \
61+
--config grafbase-postgres.toml > schema.graphql
6662
```
6763

6864
## What Gets Introspected
6965

7066
The following database objects are introspected:
7167
- Schemas
7268
- Tables
69+
- Views (normal and materialized)
7370
- Columns (including data types and constraints)
7471
- Primary keys and unique constraints
7572
- Foreign keys (relationships between tables)
@@ -84,16 +81,80 @@ The tool:
8481
4. Renders the definition as GraphQL SDL
8582
5. Outputs the SDL to stdout or a file
8683

87-
## Advanced Usage
84+
## Configuration
8885

89-
### Using a Custom Extension URL
86+
Configure the introspection command using a TOML configuration file. Include these essential settings:
9087

91-
If you need to use a custom extension URL instead of the official version:
88+
```toml
89+
# The URL of the extension, which appears at the top of the GraphQL SDL.
90+
extension_url = "https://grafbase.com/extensions/postgres/0.3.0"
9291

93-
```bash
94-
grafbase-postgres --database-url "postgres://user:pass@localhost:5432/mydb" introspect --extension-url "https://example.com/my-postgres-extension"
92+
# The default schema, which we'll omit from the SDL output.
93+
# Defaults to "public" if you don't specify it
94+
default_schema = "public"
95+
96+
# The name of the database the virtual subgraph should use. This
97+
# maps to a Postgres database name in your gateway configuration.
98+
# Defaults to "default" if you don't specify it
99+
database_name = "default"
95100
```
96101

102+
### Exposing Views
103+
104+
PostgreSQL views require additional configuration because the information schema doesn't provide details about unique constraints, nullability, or relations. To make a view visible in your GraphQL SDL, you must define at least one unique key.
105+
106+
#### Unique Key Definitions
107+
108+
```toml
109+
[schemas.public.views.my_view]
110+
# The order of columns matters - match the order in the underlying query/table.
111+
# Define compound keys like this:
112+
unique_keys = [["user_name", "user_id"]]
113+
114+
# structure: schemas.<schema_name>.views.<view_name>.columns.<column_name>
115+
[schemas.public.views.my_view.columns.user_name]
116+
# Defaults to true if you omit this setting
117+
nullable = false
118+
# Define a single-column unique key here. Defaults to false if omitted.
119+
unique = false
120+
# Customize the GraphQL field name:
121+
rename = "name_user"
122+
# Add a description that appears as a comment in the GraphQL schema:
123+
description = "The name of the user"
124+
125+
[schemas.public.views.my_view.columns.user_id]
126+
nullable = false
127+
```
128+
129+
The introspection will fail if you reference any non-existent schemas, views, or columns.
130+
131+
#### Relation Definitions
132+
133+
```toml
134+
# structure: schemas.<schema_name>.views.<view_name>.relations.<relation_name>
135+
[schemas.public.views.my_view.relations.my_view_to_my_table]
136+
# The schema containing the referenced table or view.
137+
# Defaults to "public" if omitted. Must exist.
138+
referenced_schema = "public"
139+
140+
# Specify either a table or view. Must exist.
141+
referenced_table = "my_table"
142+
143+
# List columns in the view that reference columns in the target
144+
# table or view.
145+
#
146+
# Introspection fails if these columns don't exist.
147+
referencing_columns = ["user_id", "user_name"]
148+
149+
# List columns in the target table or view that your view
150+
# references.
151+
#
152+
# Introspection fails if these columns don't exist.
153+
referenced_columns = ["id", "name"]
154+
```
155+
156+
Define these relations in your config file to enable joins to and from your views.
157+
97158
## License
98159

99160
Apache-2.0

cli/postgres/src/args.rs

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use std::path::PathBuf;
22

3-
use clap::{ArgGroup, Parser, Subcommand};
4-
use semver::Version;
5-
use url::Url;
3+
use clap::{Parser, Subcommand};
64

75
#[derive(Parser, Debug)]
86
#[command(name = "grafbase-postgres")]
@@ -24,36 +22,10 @@ pub enum Commands {
2422
}
2523

2624
#[derive(Parser, Debug)]
27-
#[command(group(
28-
ArgGroup::new("extension_identifier")
29-
.required(true)
30-
.args(["extension_url", "extension_version"]),
31-
))]
3225
pub struct IntrospectCommand {
33-
/// Output file path. If not provided, the SDL will be printed to stdout.
34-
#[arg(short, long)]
35-
pub output_file: Option<PathBuf>,
36-
/// The name of the database to be used in the GraphQL SDL
37-
#[arg(short, long, default_value = "default")]
38-
pub database_name: String,
39-
/// Default schema to be used in the GraphQL SDL (will be omitted from definitions)
40-
#[arg(short = 's', long, default_value = "public")]
41-
pub default_schema: String,
42-
/// URL to the extension
43-
#[arg(long, short = 'u')]
44-
pub extension_url: Option<Url>,
45-
/// Extension version following semver
46-
#[arg(long, short = 'v')]
47-
pub extension_version: Option<Version>,
48-
}
49-
50-
impl IntrospectCommand {
51-
pub fn extension_url(&self) -> String {
52-
match self.extension_version.as_ref() {
53-
Some(version) => format!("https://grafbase.com/extensions/postgres/{version}"),
54-
None => self.extension_url.as_ref().unwrap().to_string(),
55-
}
56-
}
26+
/// Configuration file location
27+
#[arg(short, long, default_value = "./grafbase-postgres.toml")]
28+
pub config: PathBuf,
5729
}
5830

5931
pub fn parse() -> Args {

cli/postgres/src/main.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
use anyhow::Context;
12
use args::IntrospectCommand;
2-
use grafbase_postgres_introspection::IntrospectionOptions;
33
use sqlx::{Connection, PgConnection};
44

55
mod args;
66

77
#[tokio::main(flavor = "current_thread")]
88
async fn main() -> anyhow::Result<()> {
9-
let args = args::parse();
9+
dotenv::dotenv()?;
1010

11+
let args = args::parse();
1112
let mut conn = PgConnection::connect(&args.database_url).await?;
1213

1314
match args.command {
@@ -20,18 +21,11 @@ async fn main() -> anyhow::Result<()> {
2021
}
2122

2223
async fn introspect(conn: &mut PgConnection, cmd: IntrospectCommand) -> anyhow::Result<()> {
23-
let opts = IntrospectionOptions {
24-
database_name: &cmd.database_name,
25-
extension_url: &cmd.extension_url(),
26-
default_schema: &cmd.default_schema,
27-
};
24+
let config = std::fs::read_to_string(&cmd.config).context("could not read the configuration file")?;
25+
let config = toml::from_str(&config)?;
26+
let sdl = grafbase_postgres_introspection::introspect(conn, config).await?;
2827

29-
let sdl = grafbase_postgres_introspection::introspect(conn, opts).await?;
30-
31-
match cmd.output_file {
32-
Some(path) => std::fs::write(path, sdl)?,
33-
None => println!("{sdl}"),
34-
}
28+
println!("{sdl}");
3529

3630
Ok(())
3731
}

crates/database-definition/src/key.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ impl Key<String> {
3535
}
3636

3737
impl Key<StringId> {
38-
pub(crate) fn name(&self) -> StringId {
39-
self.constraint_name
40-
}
41-
4238
pub(crate) fn r#type(&self) -> KeyType {
4339
self.r#type
4440
}

crates/database-definition/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use interner::StringInterner;
3131
pub use key::{Key, KeyType};
3232
pub use key_column::KeyColumn;
3333
use relations::Relations;
34-
pub use table::Table;
34+
pub use table::{RelationKind, Table};
3535
pub use table_column::{IdentityGeneration, TableColumn};
3636
pub use r#type::{ColumnType, DatabaseType, EnumType, ScalarKind, ScalarType};
3737
pub use walkers::{EnumWalker, KeyWalker, RelationWalker, TableColumnWalker, TableWalker, Walker};
@@ -198,6 +198,7 @@ impl DatabaseDefinition {
198198
client_name: self.interner.intern(table.client_name()),
199199
client_field_name: self.interner.intern(table.client_field_name()),
200200
client_field_name_plural: self.interner.intern(table.client_field_name_plural()),
201+
kind: table.kind,
201202
description: table.description.map(|desc| self.interner.intern(&desc)),
202203
});
203204

crates/database-definition/src/table.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,29 @@ pub struct Table<T> {
99
pub(super) client_name: T,
1010
pub(super) client_field_name: T,
1111
pub(super) client_field_name_plural: T,
12+
pub(super) kind: RelationKind,
1213
pub(super) description: Option<T>,
1314
}
1415

16+
#[derive(serde::Deserialize, Debug, Clone, Copy, Default, PartialEq)]
17+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
18+
pub enum RelationKind {
19+
#[default]
20+
Relation,
21+
View,
22+
MaterializedView,
23+
}
24+
25+
impl RelationKind {
26+
pub fn client_name(self) -> &'static str {
27+
match self {
28+
RelationKind::Relation => "RELATION",
29+
RelationKind::View => "VIEW",
30+
RelationKind::MaterializedView => "MATERIALIZED_VIEW",
31+
}
32+
}
33+
}
34+
1535
impl<T> Copy for Table<T> where T: Copy {}
1636

1737
impl<T> Table<T> {
@@ -34,10 +54,14 @@ impl<T> Table<T> {
3454
pub fn set_description(&mut self, description: T) {
3555
self.description = Some(description);
3656
}
57+
58+
pub(crate) fn kind(&self) -> RelationKind {
59+
self.kind
60+
}
3761
}
3862

3963
impl Table<String> {
40-
pub fn new(schema_id: SchemaId, database_name: String, client_name: Option<String>) -> Self {
64+
pub fn new(schema_id: SchemaId, database_name: String, kind: RelationKind, client_name: Option<String>) -> Self {
4165
let client_name = client_name.unwrap_or_else(|| database_name.to_pascal_case().to_singular());
4266
let client_field_name = client_name.to_camel_case();
4367
let client_field_name_plural = client_field_name.to_plural();
@@ -48,6 +72,7 @@ impl Table<String> {
4872
client_name,
4973
client_field_name,
5074
client_field_name_plural,
75+
kind,
5176
description: None,
5277
}
5378
}

crates/database-definition/src/walkers/key.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ impl<'a> KeyWalker<'a> {
1010
self.walk(self.get().table_id())
1111
}
1212

13-
/// The constraint name.
14-
pub fn name(self) -> &'a str {
15-
self.get_name(self.get().name())
16-
}
17-
1813
/// The columns defining the unique value.
1914
pub fn columns(self) -> impl ExactSizeIterator<Item = KeyColumnWalker<'a>> + 'a {
2015
let range = super::range_for_key(&self.database_definition.key_columns, self.id, |column| column.key_id());

0 commit comments

Comments
 (0)