Skip to content

Commit 3cdd9c5

Browse files
Merge pull request #112 from theseus-rs/add-portal-corp
feat: add portal corp extensions
2 parents dca0a06 + 4a85b26 commit 3cdd9c5

File tree

10 files changed

+299
-5
lines changed

10 files changed

+299
-5
lines changed

postgresql_extensions/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ tokio = { workspace = true, features = ["full"] }
4040
[features]
4141
default = [
4242
"native-tls",
43+
"portal-corp",
4344
"steampipe",
4445
"tensor-chord",
4546
]
4647
blocking = ["tokio"]
48+
portal-corp = [
49+
"dep:target-triple",
50+
"dep:zip",
51+
"postgresql_archive/github",
52+
]
4753
steampipe = [
4854
"dep:flate2",
4955
"dep:serde_json",

postgresql_extensions/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ The following features are available:
5353

5454
| Name | Description | Default? |
5555
|----------------|-------------------------------------------|----------|
56-
| `steampipe` | Enables Steampipe PostgreSQL extensions | No |
57-
| `tensor-chord` | Enables TensorChord PostgreSQL extensions | No |
56+
| `portal-corp` | Enables PortalCorp PostgreSQL extensions | Yes |
57+
| `steampipe` | Enables Steampipe PostgreSQL extensions | Yes |
58+
| `tensor-chord` | Enables TensorChord PostgreSQL extensions | Yes |
5859

5960
## Supported platforms
6061

postgresql_extensions/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@
5252
//!
5353
//! | Name | Description | Default? |
5454
//! |----------------|-------------------------------------------|----------|
55-
//! | `steampipe` | Enables Steampipe PostgreSQL extensions | No |
56-
//! | `tensor-chord` | Enables TensorChord PostgreSQL extensions | No |
55+
//! | `portal-corp` | Enables PortalCorp PostgreSQL extensions | Yes |
56+
//! | `steampipe` | Enables Steampipe PostgreSQL extensions | Yes |
57+
//! | `tensor-chord` | Enables TensorChord PostgreSQL extensions | Yes |
5758
//!
5859
//! ## Supported platforms
5960
//!

postgresql_extensions/src/repository/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pub mod model;
2+
#[cfg(feature = "portal-corp")]
3+
pub mod portal_corp;
24
pub mod registry;
35
#[cfg(feature = "steampipe")]
46
pub mod steampipe;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use postgresql_archive::Result;
2+
use semver::Version;
3+
use std::collections::HashMap;
4+
use url::Url;
5+
6+
/// Matcher for Portal Corp binaries from <https://github.com/portalcorp>
7+
///
8+
/// # Errors
9+
/// * If the asset matcher fails.
10+
#[allow(clippy::case_sensitive_file_extension_comparisons)]
11+
pub fn matcher(url: &str, name: &str, _version: &Version) -> Result<bool> {
12+
let Ok(url) = Url::parse(url) else {
13+
return Ok(false);
14+
};
15+
let query_parameters: HashMap<String, String> = url.query_pairs().into_owned().collect();
16+
let Some(postgresql_version) = query_parameters.get("postgresql_version") else {
17+
return Ok(false);
18+
};
19+
let postgresql_major_version = match postgresql_version.split_once('.') {
20+
None => return Ok(false),
21+
Some((major, _)) => major,
22+
};
23+
let target = target_triple::TARGET;
24+
let expected_name = format!("pgvector-{target}-pg{postgresql_major_version}.zip");
25+
Ok(name == expected_name)
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::*;
31+
use crate::repository::portal_corp;
32+
33+
#[test]
34+
fn test_match_success() -> Result<()> {
35+
let postgresql_major_version = 16;
36+
let url = format!(
37+
"{}?postgresql_version={postgresql_major_version}.6",
38+
portal_corp::URL
39+
);
40+
let version = Version::parse("0.16.12")?;
41+
let target = target_triple::TARGET;
42+
let name = format!("pgvector-{target}-pg{postgresql_major_version}.zip");
43+
44+
assert!(matcher(url.as_str(), name.as_str(), &version)?, "{}", name);
45+
Ok(())
46+
}
47+
48+
#[test]
49+
fn test_invalid_url() -> Result<()> {
50+
let url = "^";
51+
assert!(!matcher(url, "", &Version::new(0, 0, 0))?);
52+
Ok(())
53+
}
54+
55+
#[test]
56+
fn test_no_version() -> Result<()> {
57+
assert!(!matcher(portal_corp::URL, "", &Version::new(0, 0, 0))?);
58+
Ok(())
59+
}
60+
61+
#[test]
62+
fn test_invalid_version() -> Result<()> {
63+
let url = format!("{}?postgresql_version=16", portal_corp::URL);
64+
assert!(!matcher(url.as_str(), "", &Version::new(0, 0, 0))?);
65+
Ok(())
66+
}
67+
68+
#[test]
69+
fn test_match_errors() -> Result<()> {
70+
let postgresql_major_version = 16;
71+
let url = format!(
72+
"{}?postgresql_version={postgresql_major_version}.3",
73+
portal_corp::URL
74+
);
75+
let version = Version::parse("0.16.12")?;
76+
let target = target_triple::TARGET;
77+
let names = vec![
78+
format!("foo-{target}-pg{postgresql_major_version}.zip"),
79+
format!("pgvector-pg{postgresql_major_version}.zip"),
80+
format!("pgvector-{target}.zip"),
81+
format!("pgvector-{target}-pg{postgresql_major_version}.tar.gz"),
82+
];
83+
84+
for name in names {
85+
assert!(!matcher(url.as_str(), name.as_str(), &version)?, "{}", name);
86+
}
87+
Ok(())
88+
}
89+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mod matcher;
2+
pub mod repository;
3+
4+
pub const URL: &str = "https://github.com/portalcorp";
5+
6+
pub use matcher::matcher;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use crate::model::AvailableExtension;
2+
use crate::repository::portal_corp::URL;
3+
use crate::repository::{portal_corp, Repository};
4+
use crate::Result;
5+
use async_trait::async_trait;
6+
use postgresql_archive::repository::github::repository::GitHub;
7+
use postgresql_archive::{get_archive, matcher};
8+
use semver::{Version, VersionReq};
9+
use std::fmt::Debug;
10+
use std::io::Cursor;
11+
use std::path::PathBuf;
12+
use std::{fs, io};
13+
use zip::ZipArchive;
14+
15+
/// PortalCorp repository.
16+
#[derive(Debug)]
17+
pub struct PortalCorp;
18+
19+
impl PortalCorp {
20+
/// Creates a new PortalCorp repository.
21+
///
22+
/// # Errors
23+
/// * If the repository cannot be created
24+
#[allow(clippy::new_ret_no_self)]
25+
pub fn new() -> Result<Box<dyn Repository>> {
26+
Ok(Box::new(Self))
27+
}
28+
29+
/// Initializes the repository.
30+
///
31+
/// # Errors
32+
/// * If the repository cannot be initialized.
33+
pub fn initialize() -> Result<()> {
34+
matcher::registry::register(|url| Ok(url.starts_with(URL)), portal_corp::matcher)?;
35+
postgresql_archive::repository::registry::register(
36+
|url| Ok(url.starts_with(URL)),
37+
Box::new(GitHub::new),
38+
)?;
39+
Ok(())
40+
}
41+
}
42+
43+
#[async_trait]
44+
impl Repository for PortalCorp {
45+
fn name(&self) -> &str {
46+
"portal-corp"
47+
}
48+
49+
async fn get_available_extensions(&self) -> Result<Vec<AvailableExtension>> {
50+
let extensions = vec![AvailableExtension::new(
51+
self.name(),
52+
"pgvector_compiled",
53+
"Precompiled OS packages for pgvector",
54+
)];
55+
Ok(extensions)
56+
}
57+
58+
async fn get_archive(
59+
&self,
60+
postgresql_version: &str,
61+
name: &str,
62+
version: &VersionReq,
63+
) -> Result<(Version, Vec<u8>)> {
64+
let url = format!("{URL}/{name}?postgresql_version={postgresql_version}");
65+
let archive = get_archive(url.as_str(), version).await?;
66+
Ok(archive)
67+
}
68+
69+
#[allow(clippy::case_sensitive_file_extension_comparisons)]
70+
async fn install(
71+
&self,
72+
_name: &str,
73+
library_dir: PathBuf,
74+
extension_dir: PathBuf,
75+
archive: &[u8],
76+
) -> Result<Vec<PathBuf>> {
77+
let reader = Cursor::new(archive);
78+
let mut archive = ZipArchive::new(reader)
79+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Zip error"))?;
80+
let mut files = Vec::new();
81+
82+
for i in 0..archive.len() {
83+
let mut file = archive
84+
.by_index(i)
85+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Zip error"))?;
86+
let file_path = PathBuf::from(file.name());
87+
let file_path = PathBuf::from(file_path.file_name().unwrap_or_default());
88+
let file_name = file_path.to_string_lossy();
89+
90+
if file_name.ends_with(".dll")
91+
|| file_name.ends_with(".dylib")
92+
|| file_name.ends_with(".so")
93+
{
94+
let mut out = Vec::new();
95+
io::copy(&mut file, &mut out)?;
96+
let path = PathBuf::from(&library_dir).join(file_path);
97+
fs::write(&path, out)?;
98+
files.push(path);
99+
} else if file_name.ends_with(".control") || file_name.ends_with(".sql") {
100+
let mut out = Vec::new();
101+
io::copy(&mut file, &mut out)?;
102+
let path = PathBuf::from(&extension_dir).join(file_path);
103+
fs::write(&path, out)?;
104+
files.push(path);
105+
}
106+
}
107+
108+
Ok(files)
109+
}
110+
}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use super::*;
115+
use crate::repository::Repository;
116+
117+
#[test]
118+
fn test_name() {
119+
let repository = PortalCorp;
120+
assert_eq!("portal-corp", repository.name());
121+
}
122+
123+
#[tokio::test]
124+
async fn test_get_available_extensions() -> Result<()> {
125+
let repository = PortalCorp;
126+
let extensions = repository.get_available_extensions().await?;
127+
let extension = &extensions[0];
128+
129+
assert_eq!("pgvector_compiled", extension.name());
130+
assert_eq!(
131+
"Precompiled OS packages for pgvector",
132+
extension.description()
133+
);
134+
Ok(())
135+
}
136+
}

postgresql_extensions/src/repository/registry.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use crate::repository::model::Repository;
2+
#[cfg(feature = "portal-corp")]
3+
use crate::repository::portal_corp::repository::PortalCorp;
4+
#[cfg(feature = "steampipe")]
25
use crate::repository::steampipe::repository::Steampipe;
6+
#[cfg(feature = "tensor-chord")]
37
use crate::repository::tensor_chord::repository::TensorChord;
48
use crate::Error::{PoisonedLock, UnsupportedNamespace};
59
use crate::Result;
@@ -52,6 +56,11 @@ impl Default for RepositoryRegistry {
5256
/// Creates a new repository registry with the default repositories registered.
5357
fn default() -> Self {
5458
let mut registry = Self::new();
59+
#[cfg(feature = "portal-corp")]
60+
{
61+
registry.register("portal-corp", Box::new(PortalCorp::new));
62+
let _ = PortalCorp::initialize();
63+
}
5564
#[cfg(feature = "steampipe")]
5665
{
5766
registry.register("steampipe", Box::new(Steampipe::new));
@@ -179,6 +188,12 @@ mod tests {
179188
assert_eq!("unsupported namespace 'foo'", error.to_string());
180189
}
181190

191+
#[test]
192+
#[cfg(feature = "portal-corp")]
193+
fn test_get_portal_corp_extensions() {
194+
assert!(get("portal-corp").is_ok());
195+
}
196+
182197
#[test]
183198
#[cfg(feature = "steampipe")]
184199
fn test_get_steampipe_extensions() {
@@ -194,6 +209,8 @@ mod tests {
194209
#[test]
195210
fn test_get_namespaces() {
196211
let namespaces = get_namespaces().unwrap();
212+
#[cfg(feature = "portal-corp")]
213+
assert!(namespaces.contains(&"portal-corp".to_string()));
197214
#[cfg(feature = "steampipe")]
198215
assert!(namespaces.contains(&"steampipe".to_string()));
199216
#[cfg(feature = "tensor-chord")]

postgresql_extensions/src/repository/steampipe/matcher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub fn matcher(url: &str, name: &str, _version: &Version) -> Result<bool> {
3434
fn get_os() -> &'static str {
3535
match consts::OS {
3636
"macos" => "darwin",
37-
_ => "linux",
37+
_ => consts::OS,
3838
}
3939
}
4040

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))]
2+
#[cfg(feature = "portal-corp")]
3+
#[tokio::test]
4+
async fn test_lifecycle() -> anyhow::Result<()> {
5+
let installation_dir = tempfile::tempdir()?.path().to_path_buf();
6+
let postgresql_version = semver::VersionReq::parse("=16.3.0")?;
7+
let settings = postgresql_embedded::Settings {
8+
version: postgresql_version,
9+
installation_dir: installation_dir.clone(),
10+
..Default::default()
11+
};
12+
let mut postgresql = postgresql_embedded::PostgreSQL::new(settings);
13+
14+
postgresql.setup().await?;
15+
16+
let settings = postgresql.settings();
17+
let namespace = "portal-corp";
18+
let name = "pgvector_compiled";
19+
let version = semver::VersionReq::parse("=0.16.12")?;
20+
21+
let installed_extensions = postgresql_extensions::get_installed_extensions(settings).await?;
22+
assert!(installed_extensions.is_empty());
23+
24+
postgresql_extensions::install(settings, namespace, name, &version).await?;
25+
26+
let installed_extensions = postgresql_extensions::get_installed_extensions(settings).await?;
27+
assert!(!installed_extensions.is_empty());
28+
29+
postgresql_extensions::uninstall(settings, namespace, name).await?;
30+
31+
let installed_extensions = postgresql_extensions::get_installed_extensions(settings).await?;
32+
assert!(installed_extensions.is_empty());
33+
34+
tokio::fs::remove_dir_all(&installation_dir).await?;
35+
Ok(())
36+
}

0 commit comments

Comments
 (0)