1use std::collections::BTreeSet;
2
3use anyhow::{Result, bail};
4
5#[path = "generated_extensions.rs"]
6mod generated;
7
8pub use generated::*;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct Extension {
13 name: &'static str,
14 sql_name: &'static str,
15 archive_name: &'static str,
16 native_module_file: Option<&'static str>,
17 aot_name: Option<&'static str>,
18 dependencies: &'static [&'static str],
19 setup: ExtensionSetup,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub(crate) struct ExtensionSetup {
24 create_extension: bool,
25 create_schema: Option<&'static str>,
26 load_sql: &'static [&'static str],
27 post_create_sql: &'static [&'static str],
28}
29
30impl ExtensionSetup {
31 pub(crate) const fn new(
32 create_extension: bool,
33 create_schema: Option<&'static str>,
34 load_sql: &'static [&'static str],
35 post_create_sql: &'static [&'static str],
36 ) -> Self {
37 Self {
38 create_extension,
39 create_schema,
40 load_sql,
41 post_create_sql,
42 }
43 }
44}
45
46impl Extension {
47 #[allow(dead_code)]
48 pub(crate) const fn new(
49 name: &'static str,
50 sql_name: &'static str,
51 archive_name: &'static str,
52 native_module_file: Option<&'static str>,
53 aot_name: Option<&'static str>,
54 dependencies: &'static [&'static str],
55 setup: ExtensionSetup,
56 ) -> Self {
57 Self {
58 name,
59 sql_name,
60 archive_name,
61 native_module_file,
62 aot_name,
63 dependencies,
64 setup,
65 }
66 }
67
68 pub const fn name(self) -> &'static str {
70 self.name
71 }
72
73 pub const fn sql_name(self) -> &'static str {
75 self.sql_name
76 }
77
78 pub const fn archive_name(self) -> &'static str {
80 self.archive_name
81 }
82
83 pub const fn aot_name(self) -> Option<&'static str> {
85 self.aot_name
86 }
87
88 pub const fn native_module_file(self) -> Option<&'static str> {
91 self.native_module_file
92 }
93
94 pub const fn dependencies(self) -> &'static [&'static str] {
96 self.dependencies
97 }
98
99 pub(crate) const fn setup(self) -> ExtensionSetup {
100 self.setup
101 }
102}
103
104pub fn by_sql_name(sql_name: &str) -> Option<Extension> {
105 ALL.iter()
106 .copied()
107 .find(|extension| extension.sql_name == sql_name)
108}
109
110pub(crate) fn candidate_by_sql_name(sql_name: &str) -> Option<Extension> {
111 generated::CANDIDATES
112 .iter()
113 .copied()
114 .find(|extension| extension.sql_name == sql_name)
115}
116
117pub(crate) fn resolve_extension_set(extensions: &[Extension]) -> Result<Vec<Extension>> {
118 let mut visiting = BTreeSet::new();
119 let mut visited = BTreeSet::new();
120 let mut resolved = Vec::new();
121 let mut requested = extensions.to_vec();
122 requested.sort_by_key(|extension| extension.sql_name());
123 for extension in requested {
124 visit_extension(extension, &mut visiting, &mut visited, &mut resolved)?;
125 }
126 Ok(resolved)
127}
128
129fn visit_extension(
130 extension: Extension,
131 visiting: &mut BTreeSet<&'static str>,
132 visited: &mut BTreeSet<&'static str>,
133 resolved: &mut Vec<Extension>,
134) -> Result<()> {
135 if visited.contains(extension.sql_name()) {
136 return Ok(());
137 }
138 if !visiting.insert(extension.sql_name()) {
139 bail!(
140 "cyclic bundled extension dependency involving '{}'",
141 extension.sql_name()
142 );
143 }
144 for dependency in extension.dependencies() {
145 let dependency_extension = candidate_by_sql_name(dependency).ok_or_else(|| {
146 anyhow::anyhow!(
147 "bundled extension '{}' depends on missing packaged extension '{}'",
148 extension.sql_name(),
149 dependency
150 )
151 })?;
152 visit_extension(dependency_extension, visiting, visited, resolved)?;
153 }
154 visiting.remove(extension.sql_name());
155 visited.insert(extension.sql_name());
156 resolved.push(extension);
157 Ok(())
158}
159
160pub(crate) fn extension_setup_sql(extension: Extension) -> Vec<String> {
161 let setup = extension.setup();
162 let mut statements = Vec::new();
163 if setup.create_extension {
164 if let Some(schema) = setup.create_schema.filter(|schema| *schema != "pg_catalog") {
165 statements.push(format!(
166 "CREATE SCHEMA IF NOT EXISTS {};",
167 crate::pglite::templating::quote_identifier(schema)
168 ));
169 }
170 let mut sql = format!(
171 "CREATE EXTENSION IF NOT EXISTS {}",
172 crate::pglite::templating::quote_identifier(extension.sql_name())
173 );
174 if let Some(schema) = setup.create_schema {
175 sql.push_str(" WITH SCHEMA ");
176 sql.push_str(&crate::pglite::templating::quote_identifier(schema));
177 }
178 sql.push(';');
179 statements.push(sql);
180 }
181 statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned()));
182 statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned()));
183 statements
184}
185
186pub(crate) fn extension_session_setup_sql(extension: Extension) -> Vec<String> {
187 let setup = extension.setup();
188 let mut statements = Vec::new();
189 statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned()));
190 statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned()));
191 statements
192}
193
194#[cfg(all(test, feature = "extensions"))]
195mod candidate_tests {
196 use super::*;
197 use crate::{Pglite, PgliteServer};
198 use anyhow::{Context, Result, ensure};
199 use sqlx::{Connection, PgConnection};
200 use std::collections::BTreeSet;
201 use std::path::{Path, PathBuf};
202
203 #[test]
204 fn public_extensions_pass_direct_and_restart_smoke() -> Result<()> {
205 run_direct_and_restart_smoke_set(generated::ALL)
206 }
207
208 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
209 async fn public_extensions_pass_server_smoke() -> Result<()> {
210 run_server_smoke_set(generated::ALL).await
211 }
212
213 #[test]
214 fn public_extensions_materialize_only_requested_libraries() -> Result<()> {
215 run_lifecycle_materialization_set(generated::ALL)
216 }
217
218 #[test]
219 #[ignore = "promotion gate: run manually before marking packaged candidates stable"]
220 fn packaged_candidate_extensions_pass_direct_and_restart_smoke() -> Result<()> {
221 run_direct_and_restart_smoke_set(generated::CANDIDATES)
222 }
223
224 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
225 #[ignore = "promotion gate: run manually before marking packaged candidates stable"]
226 async fn packaged_candidate_extensions_pass_server_smoke() -> Result<()> {
227 run_server_smoke_set(generated::CANDIDATES).await
228 }
229
230 #[test]
231 #[ignore = "promotion gate: run manually before marking packaged candidates stable"]
232 fn packaged_candidate_extensions_materialize_only_requested_libraries() -> Result<()> {
233 run_lifecycle_materialization_set(generated::CANDIDATES)
234 }
235
236 fn run_direct_and_restart_smoke_set(extensions: &[Extension]) -> Result<()> {
237 let mut failures = Vec::new();
238 for extension in extensions {
239 if let Err(error) = run_one_direct_and_restart_smoke(*extension) {
240 failures.push(format!("{}: {error:?}", extension.sql_name()));
241 }
242 }
243 ensure!(
244 failures.is_empty(),
245 "extension direct/restart smoke failures:\n{}",
246 failures.join("\n\n")
247 );
248 Ok(())
249 }
250
251 fn run_one_direct_and_restart_smoke(extension: Extension) -> Result<()> {
252 let name = extension.sql_name();
253 {
254 let mut db = Pglite::builder()
255 .temporary()
256 .extension(extension)
257 .open()
258 .with_context(|| format!("open temporary database with extension {name}"))?;
259 run_direct_smoke(&mut db, extension)?;
260 db.close()
261 .with_context(|| format!("close temporary database with extension {name}"))?;
262 }
263
264 let root = tempfile::TempDir::new()
265 .with_context(|| format!("create restart root for extension {name}"))?;
266 {
267 let mut db = Pglite::builder()
268 .path(root.path())
269 .extension(extension)
270 .open()
271 .with_context(|| {
272 format!("open persistent database with extension {name} before restart")
273 })?;
274 run_direct_smoke(&mut db, extension)?;
275 assert_extension_catalog_state(&mut db, extension)?;
276 db.close()
277 .with_context(|| format!("close persistent database with extension {name}"))?;
278 }
279 {
280 let mut db = Pglite::builder()
281 .path(root.path())
282 .extension(extension)
283 .open()
284 .with_context(|| {
285 format!("reopen persistent database with extension {name} after restart")
286 })?;
287 assert_extension_catalog_state(&mut db, extension)?;
288 db.close()
289 .with_context(|| format!("close restarted database with extension {name}"))?;
290 }
291 Ok(())
292 }
293
294 async fn run_server_smoke_set(extensions: &[Extension]) -> Result<()> {
295 let mut failures = Vec::new();
296 for extension in extensions {
297 if let Err(error) = run_one_server_smoke(*extension).await {
298 failures.push(format!("{}: {error:?}", extension.sql_name()));
299 }
300 }
301 ensure!(
302 failures.is_empty(),
303 "extension server smoke failures:\n{}",
304 failures.join("\n\n")
305 );
306 Ok(())
307 }
308
309 async fn run_one_server_smoke(extension: Extension) -> Result<()> {
310 let name = extension.sql_name();
311 let server = PgliteServer::builder()
312 .temporary()
313 .extension(extension)
314 .start()
315 .with_context(|| format!("start server with extension {name}"))?;
316 let mut conn = PgConnection::connect(&server.database_url())
317 .await
318 .with_context(|| format!("connect server with extension {name}"))?;
319 run_server_smoke(&mut conn, extension).await?;
320 drop(conn);
321 server
322 .shutdown()
323 .with_context(|| format!("shutdown server with extension {name}"))?;
324 Ok(())
325 }
326
327 fn run_lifecycle_materialization_set(extensions: &[Extension]) -> Result<()> {
328 let mut failures = Vec::new();
329 for extension in extensions {
330 if let Err(error) = run_one_lifecycle_materialization(*extension) {
331 failures.push(format!("{}: {error:?}", extension.sql_name()));
332 }
333 }
334 ensure!(
335 failures.is_empty(),
336 "extension lifecycle/materialization failures:\n{}",
337 failures.join("\n\n")
338 );
339 Ok(())
340 }
341
342 fn run_one_lifecycle_materialization(extension: Extension) -> Result<()> {
343 let name = extension.sql_name();
344 let root = tempfile::TempDir::new()
345 .with_context(|| format!("create lifecycle root for extension {name}"))?;
346 {
347 let mut db = Pglite::builder()
348 .path(root.path())
349 .extension(extension)
350 .open()
351 .with_context(|| format!("open lifecycle database with extension {name}"))?;
352 db.close()
353 .with_context(|| format!("close lifecycle database with extension {name}"))?;
354 }
355 assert_only_resolved_extension_libraries_are_materialized(root.path(), extension)
356 }
357
358 fn run_direct_smoke(db: &mut Pglite, extension: Extension) -> Result<()> {
359 for statement in smoke_sql(extension.sql_name()) {
360 db.exec(statement, None).with_context(|| {
361 format!(
362 "direct smoke failed for extension {} while running:\n{}",
363 extension.sql_name(),
364 statement
365 )
366 })?;
367 }
368 Ok(())
369 }
370
371 async fn run_server_smoke(conn: &mut PgConnection, extension: Extension) -> Result<()> {
372 for statement in smoke_sql(extension.sql_name()) {
373 sqlx::query(statement)
374 .fetch_all(&mut *conn)
375 .await
376 .with_context(|| {
377 format!(
378 "server smoke failed for extension {} while running:\n{}",
379 extension.sql_name(),
380 statement
381 )
382 })?;
383 }
384 Ok(())
385 }
386
387 fn assert_extension_catalog_state(db: &mut Pglite, extension: Extension) -> Result<()> {
388 if extension.setup().create_extension {
389 let result = db.query(
390 "SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = $1",
391 &[serde_json::json!(extension.sql_name())],
392 None,
393 )?;
394 ensure!(
395 result.rows[0]["count"] == serde_json::json!(1),
396 "extension {} should survive restart in pg_extension",
397 extension.sql_name()
398 );
399 } else {
400 let result = db.query("SELECT 1::int4 AS ok", &[], None)?;
401 ensure!(
402 result.rows[0]["ok"] == serde_json::json!(1),
403 "extension {} should reopen cleanly",
404 extension.sql_name()
405 );
406 }
407 Ok(())
408 }
409
410 fn assert_only_resolved_extension_libraries_are_materialized(
411 root: &Path,
412 extension: Extension,
413 ) -> Result<()> {
414 let expected = resolve_extension_set(&[extension])?
415 .into_iter()
416 .filter_map(|extension| extension.native_module_file().map(PathBuf::from))
417 .collect::<BTreeSet<_>>();
418 let actual = relative_files(&root.join("tmp/pglite/lib/postgresql"))
419 .into_iter()
420 .collect::<BTreeSet<_>>();
421 ensure!(
422 actual == expected,
423 "upper runtime library layer for {} should contain only resolved requested libraries; expected {:?}, got {:?}",
424 extension.sql_name(),
425 expected,
426 actual
427 );
428 Ok(())
429 }
430
431 fn relative_files(root: &Path) -> Vec<PathBuf> {
432 fn walk(base: &Path, current: &Path, files: &mut Vec<PathBuf>) {
433 let Ok(entries) = std::fs::read_dir(current) else {
434 return;
435 };
436 for entry in entries {
437 let entry = entry.expect("read runtime test directory entry");
438 let path = entry.path();
439 if path.is_dir() {
440 walk(base, &path, files);
441 } else if path.is_file() {
442 files.push(
443 path.strip_prefix(base)
444 .expect("relative extension library path")
445 .to_path_buf(),
446 );
447 }
448 }
449 }
450
451 let mut files = Vec::new();
452 walk(root, root, &mut files);
453 files.sort();
454 files
455 }
456
457 fn smoke_sql(sql_name: &str) -> &'static [&'static str] {
458 match sql_name {
461 "age" => &[
462 "SELECT ag_catalog.create_graph('oxide_graph')",
463 "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM ag_catalog.ag_graph WHERE name = 'oxide_graph') THEN RAISE EXCEPTION 'age graph was not created'; END IF; END $$",
464 "SELECT * FROM ag_catalog.cypher('oxide_graph', $$ RETURN 1 $$) AS (one agtype)",
465 ],
466 "amcheck" => &[
467 "CREATE TEMP TABLE oxide_amcheck (id int PRIMARY KEY, value text)",
468 "INSERT INTO oxide_amcheck SELECT i, 'v' || i::text FROM generate_series(1, 8) AS i",
469 "SELECT bt_index_check('oxide_amcheck_pkey'::regclass)",
470 ],
471 "auto_explain" => &["EXPLAIN SELECT count(*) FROM pg_class"],
472 "bloom" => &[
473 "CREATE TEMP TABLE oxide_bloom (id int, value int)",
474 "CREATE INDEX oxide_bloom_idx ON oxide_bloom USING bloom (id, value)",
475 "INSERT INTO oxide_bloom SELECT i, i % 3 FROM generate_series(1, 20) AS i",
476 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_bloom WHERE id = 7 AND value = 1; IF n <> 1 THEN RAISE EXCEPTION 'bloom lookup failed: %', n; END IF; END $$",
477 ],
478 "btree_gin" => &[
479 "CREATE TEMP TABLE oxide_btree_gin (id int)",
480 "CREATE INDEX oxide_btree_gin_idx ON oxide_btree_gin USING gin (id)",
481 "INSERT INTO oxide_btree_gin SELECT generate_series(1, 10)",
482 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gin WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gin lookup failed: %', n; END IF; END $$",
483 ],
484 "btree_gist" => &[
485 "CREATE TEMP TABLE oxide_btree_gist (id int)",
486 "CREATE INDEX oxide_btree_gist_idx ON oxide_btree_gist USING gist (id)",
487 "INSERT INTO oxide_btree_gist SELECT generate_series(1, 10)",
488 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gist WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gist lookup failed: %', n; END IF; END $$",
489 ],
490 "citext" => &[
491 "CREATE TEMP TABLE oxide_citext (value citext)",
492 "INSERT INTO oxide_citext VALUES ('Postgres')",
493 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_citext WHERE value = 'postgres'; IF n <> 1 THEN RAISE EXCEPTION 'citext comparison failed: %', n; END IF; END $$",
494 ],
495 "cube" => &[
496 "DO $$ DECLARE d float8; BEGIN SELECT cube(array[1,2,3]) <-> cube(array[1,2,4]) INTO d; IF d <> 1 THEN RAISE EXCEPTION 'cube distance failed: %', d; END IF; END $$",
497 ],
498 "dict_int" => &[
499 "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('intdict', '40865854'), ',') INTO lex; IF lex <> '408658' THEN RAISE EXCEPTION 'dict_int lexize failed: %', lex; END IF; END $$",
500 ],
501 "dict_xsyn" => &[
502 "ALTER TEXT SEARCH DICTIONARY xsyn (RULES = 'xsyn_sample', KEEPORIG = true, MATCHORIG = true, KEEPSYNONYMS = true, MATCHSYNONYMS = false)",
503 "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('xsyn', 'supernova'), ',') INTO lex; IF lex IS NULL OR lex !~ 'sn' THEN RAISE EXCEPTION 'dict_xsyn lexize failed: %', lex; END IF; END $$",
504 ],
505 "earthdistance" => &[
506 "DO $$ DECLARE d float8; BEGIN SELECT earth_distance(ll_to_earth(0, 0), ll_to_earth(0, 1)) INTO d; IF d <= 0 THEN RAISE EXCEPTION 'earthdistance failed: %', d; END IF; END $$",
507 ],
508 "file_fdw" => &[
509 "CREATE SERVER oxide_file_server FOREIGN DATA WRAPPER file_fdw",
510 "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_foreign_data_wrapper WHERE fdwname = 'file_fdw') THEN RAISE EXCEPTION 'file_fdw wrapper missing'; END IF; END $$",
511 ],
512 "fuzzystrmatch" => &[
513 "DO $$ BEGIN IF levenshtein('kitten', 'sitting') <> 3 THEN RAISE EXCEPTION 'levenshtein failed'; END IF; IF soundex('kitten') <> 'K350' THEN RAISE EXCEPTION 'soundex failed'; END IF; END $$",
514 ],
515 "hstore" => &[
516 "CREATE TEMP TABLE oxide_hstore (attrs hstore)",
517 "INSERT INTO oxide_hstore VALUES ('a=>1,b=>2'::hstore)",
518 "DO $$ DECLARE v text; BEGIN SELECT attrs -> 'b' INTO v FROM oxide_hstore; IF v <> '2' THEN RAISE EXCEPTION 'hstore lookup failed: %', v; END IF; END $$",
519 ],
520 "intarray" => &[
521 "CREATE TEMP TABLE oxide_intarray (tags int[])",
522 "INSERT INTO oxide_intarray VALUES (ARRAY[1, 2, 5]), (ARRAY[3, 4])",
523 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_intarray WHERE tags && ARRAY[2, 9]; IF n <> 1 THEN RAISE EXCEPTION 'intarray overlap failed: %', n; END IF; SELECT count(*) INTO n FROM oxide_intarray WHERE tags @@ '1 & (2|3)'::query_int; IF n <> 1 THEN RAISE EXCEPTION 'intarray query_int failed: %', n; END IF; END $$",
524 ],
525 "isn" => &[
526 "DO $$ BEGIN IF isbn('978-0-393-04002-9')::text <> '0-393-04002-X' THEN RAISE EXCEPTION 'isbn failed'; END IF; IF isbn13('0901690546')::text <> '978-0-901690-54-8' THEN RAISE EXCEPTION 'isbn13 failed'; END IF; IF issn('1436-4522')::text <> '1436-4522' THEN RAISE EXCEPTION 'issn failed'; END IF; END $$",
527 ],
528 "lo" => &[
529 "CREATE TEMP TABLE oxide_lo (id int, data oid)",
530 "CREATE TRIGGER oxide_lo_manage BEFORE UPDATE OR DELETE ON oxide_lo FOR EACH ROW EXECUTE FUNCTION lo_manage(data)",
531 "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'oxide_lo_manage') THEN RAISE EXCEPTION 'lo trigger missing'; END IF; END $$",
532 ],
533 "ltree" => &[
534 "CREATE TEMP TABLE oxide_ltree (path ltree)",
535 "INSERT INTO oxide_ltree VALUES ('Top.Science.Astronomy'), ('Top.Collections.Pictures')",
536 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ltree WHERE path <@ 'Top.Science'; IF n <> 1 THEN RAISE EXCEPTION 'ltree ancestor query failed: %', n; END IF; END $$",
537 ],
538 "pageinspect" => &[
539 "CREATE TEMP TABLE oxide_pageinspect (id int)",
540 "INSERT INTO oxide_pageinspect SELECT generate_series(1, 5)",
541 "SELECT * FROM page_header(get_raw_page('oxide_pageinspect', 0))",
542 ],
543 "pg_buffercache" => &[
544 "SELECT * FROM pg_buffercache_summary()",
545 "SELECT * FROM pg_buffercache_usage_counts()",
546 ],
547 "pg_freespacemap" => &[
548 "CREATE TEMP TABLE oxide_fsm (id int, value text)",
549 "INSERT INTO oxide_fsm SELECT i, repeat('x', 200) FROM generate_series(1, 20) AS i",
550 "DELETE FROM oxide_fsm WHERE id % 2 = 0",
551 "SELECT * FROM pg_freespace('oxide_fsm') LIMIT 1",
552 ],
553 "pg_hashids" => &[
554 "DO $$ BEGIN IF id_encode(1001) <> 'jNl' THEN RAISE EXCEPTION 'pg_hashids encode failed'; END IF; IF id_decode_once('jNl') <> 1001 THEN RAISE EXCEPTION 'pg_hashids decode failed'; END IF; END $$",
555 ],
556 "pg_ivm" => &[
557 "CREATE TABLE oxide_ivm_orders (id int, amount int)",
558 "INSERT INTO oxide_ivm_orders VALUES (1, 10), (2, 20)",
559 "SELECT pgivm.create_immv('oxide_ivm_summary', $$ SELECT id, amount FROM oxide_ivm_orders $$)",
560 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ivm_summary; IF n <> 2 THEN RAISE EXCEPTION 'pg_ivm initial count failed: %', n; END IF; END $$",
561 ],
562 "pg_surgery" => &[
563 "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'heap_force_kill') THEN RAISE EXCEPTION 'pg_surgery function missing'; END IF; END $$",
564 ],
565 "pg_textsearch" => &[
566 "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_am WHERE amname = 'bm25') THEN RAISE EXCEPTION 'bm25 access method missing'; END IF; END $$",
567 "SELECT to_bm25query('postgres wasm')",
568 ],
569 "pg_trgm" => &[
570 "DO $$ DECLARE score float8; BEGIN SELECT similarity('postgres', 'postgrex') INTO score; IF score <= 0 THEN RAISE EXCEPTION 'pg_trgm similarity failed: %', score; END IF; END $$",
571 ],
572 "pg_uuidv7" => &[
573 "DO $$ DECLARE id uuid; ts timestamptz; BEGIN SELECT uuid_generate_v7() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuidv7 length failed'; END IF; SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') INTO ts; IF ts IS NULL THEN RAISE EXCEPTION 'uuidv7 timestamp failed'; END IF; END $$",
574 ],
575 "pg_visibility" => &[
576 "CREATE TEMP TABLE oxide_visibility (id int)",
577 "INSERT INTO oxide_visibility SELECT generate_series(1, 5)",
578 "SELECT * FROM pg_visibility('oxide_visibility') LIMIT 1",
579 "SELECT * FROM pg_visibility_map('oxide_visibility') LIMIT 1",
580 ],
581 "pg_walinspect" => &[
582 "CREATE TEMP TABLE oxide_walinspect (value text)",
583 "CREATE TEMP TABLE oxide_walinspect_lsn AS SELECT pg_current_wal_lsn() AS before_lsn",
584 "INSERT INTO oxide_walinspect SELECT 'row ' || i::text FROM generate_series(1, 5) AS i",
585 "SELECT * FROM pg_get_wal_block_info((SELECT before_lsn FROM oxide_walinspect_lsn), pg_current_wal_lsn()) ORDER BY start_lsn, block_id LIMIT 20",
586 ],
587 "pgtap" => &[
588 "BEGIN",
589 "SELECT plan(1)",
590 "SELECT pass('pgtap smoke')",
591 "SELECT * FROM finish()",
592 "ROLLBACK",
593 ],
594 "seg" => &[
595 "DO $$ BEGIN IF '7(+-)1'::seg::text <> '6 .. 8' THEN RAISE EXCEPTION 'seg cast failed'; END IF; END $$",
596 ],
597 "tablefunc" => &[
598 "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM normal_rand(10, 5, 3); IF n <> 10 THEN RAISE EXCEPTION 'normal_rand failed: %', n; END IF; END $$",
599 "SELECT * FROM crosstab('SELECT 1, 1, 10 UNION ALL SELECT 1, 2, 20') AS ct(rowid int, c1 int, c2 int)",
600 ],
601 "tcn" => &[
602 "CREATE TEMP TABLE oxide_tcn (id int PRIMARY KEY, value text)",
603 "CREATE TRIGGER oxide_tcn_trigger AFTER INSERT OR UPDATE OR DELETE ON oxide_tcn FOR EACH ROW EXECUTE FUNCTION triggered_change_notification()",
604 "INSERT INTO oxide_tcn VALUES (1, 'one')",
605 ],
606 "tsm_system_rows" => &[
607 "CREATE TEMP TABLE oxide_tsm_rows AS SELECT i FROM generate_series(1, 20) AS i",
608 "SELECT * FROM oxide_tsm_rows TABLESAMPLE SYSTEM_ROWS(5)",
609 ],
610 "tsm_system_time" => &[
611 "CREATE TEMP TABLE oxide_tsm_time AS SELECT i FROM generate_series(1, 20) AS i",
612 "SELECT * FROM oxide_tsm_time TABLESAMPLE SYSTEM_TIME(50)",
613 ],
614 "unaccent" => &[
615 "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('unaccent', 'Hôtel'), ',') INTO lex; IF lex <> 'Hotel' THEN RAISE EXCEPTION 'unaccent failed: %', lex; END IF; END $$",
616 ],
617 "vector" => &[
618 "CREATE TEMP TABLE oxide_vector (embedding vector(3))",
619 "INSERT INTO oxide_vector VALUES ('[1,2,3]')",
620 "DO $$ DECLARE d float8; BEGIN SELECT embedding <-> '[1,2,4]'::vector INTO d FROM oxide_vector; IF d <> 1 THEN RAISE EXCEPTION 'vector distance failed: %', d; END IF; END $$",
621 ],
622 other => panic!("missing smoke SQL for packaged extension candidate {other}"),
623 }
624 }
625}