-- biscuit--1.0.sql -- SQL installation script for Biscuit Index Access Method -- PostgreSQL 15+ compatible with full CRUD support and multi-column indexes -- -- Features: -- - O(1) lazy deletion with tombstones -- - Incremental insert/update -- - Automatic slot reuse -- - Full VACUUM integration -- - Optimized pattern matching for LIKE queries -- - Multi-column index support -- - Mixed data type support -- Complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION biscuit" to load this file. \quit -- ==================== CORE INDEX ACCESS METHOD ==================== -- Create the index access method handler function CREATE FUNCTION biscuit_handler(internal) RETURNS index_am_handler AS 'MODULE_PATHNAME', 'biscuit_handler' LANGUAGE C STRICT; COMMENT ON FUNCTION biscuit_handler(internal) IS 'Index access method handler for Biscuit indexes - provides callbacks for index operations'; -- Create the Biscuit index access method CREATE ACCESS METHOD biscuit TYPE INDEX HANDLER biscuit_handler; COMMENT ON ACCESS METHOD biscuit IS 'Biscuit index access method: High-performance pattern matching for LIKE queries with multi-column support'; -- ==================== OPERATOR SUPPORT ==================== -- Support function for LIKE operator optimization CREATE FUNCTION biscuit_like_support(internal) RETURNS bool AS 'MODULE_PATHNAME', 'biscuit_like_support' LANGUAGE C STRICT; COMMENT ON FUNCTION biscuit_like_support(internal) IS 'Support function that tells the planner Biscuit can handle LIKE pattern matching'; -- ==================== DIAGNOSTIC FUNCTIONS ==================== -- Function to get index statistics and health information CREATE FUNCTION biscuit_index_stats(oid) RETURNS text AS 'MODULE_PATHNAME', 'biscuit_index_stats' LANGUAGE C STRICT; COMMENT ON FUNCTION biscuit_index_stats(oid) IS 'Returns detailed statistics for a Biscuit index including CRUD counts, tombstones, and memory usage. Usage: SELECT biscuit_index_stats(''index_name''::regclass::oid);'; -- ==================== OPERATOR CLASSES ==================== -- Default operator class for text types (text, varchar, bpchar) CREATE OPERATOR CLASS biscuit_text_ops DEFAULT FOR TYPE text USING biscuit AS OPERATOR 1 ~~ (text, text), -- LIKE operator OPERATOR 2 ~~* (text, text), -- ILIKE operator (case-insensitive) FUNCTION 1 biscuit_like_support(internal); COMMENT ON OPERATOR CLASS biscuit_text_ops USING biscuit IS 'Default operator class for Biscuit indexes on text columns - supports LIKE and ILIKE queries'; -- Operator class for VARCHAR CREATE OPERATOR CLASS biscuit_varchar_ops DEFAULT FOR TYPE varchar USING biscuit AS OPERATOR 1 ~~ (text, text), OPERATOR 2 ~~* (text, text), FUNCTION 1 biscuit_like_support(internal); -- Operator class for CHAR CREATE OPERATOR CLASS biscuit_bpchar_ops DEFAULT FOR TYPE bpchar USING biscuit AS OPERATOR 1 ~~ (text, text), OPERATOR 2 ~~* (text, text), FUNCTION 1 biscuit_like_support(internal); -- ==================== HELPER VIEWS ==================== -- View to show all Biscuit indexes in the database CREATE VIEW biscuit_indexes AS SELECT n.nspname AS schema_name, c.relname AS index_name, t.relname AS table_name, CASE WHEN array_length(i.indkey, 1) = 1 THEN (SELECT a.attname FROM pg_attribute a WHERE a.attrelid = t.oid AND a.attnum = i.indkey[0]) ELSE array_length(i.indkey, 1)::text || ' columns' END AS columns, array_length(i.indkey, 1) AS num_columns, pg_size_pretty(pg_relation_size(c.oid)) AS index_size, c.oid AS index_oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_am am ON am.oid = c.relam JOIN pg_index i ON i.indexrelid = c.oid JOIN pg_class t ON t.oid = i.indrelid WHERE am.amname = 'biscuit' AND c.relkind = 'i' ORDER BY n.nspname, c.relname; COMMENT ON VIEW biscuit_indexes IS 'Shows all Biscuit indexes in the current database with their tables, columns, and sizes. Supports multi-column indexes.'; -- Detailed multi-column index view CREATE VIEW biscuit_indexes_detailed AS SELECT n.nspname AS schema_name, c.relname AS index_name, t.relname AS table_name, array_length(i.indkey, 1) AS num_columns, array_agg(a.attname ORDER BY array_position(i.indkey, a.attnum)) AS column_names, array_agg(format_type(a.atttypid, a.atttypmod) ORDER BY array_position(i.indkey, a.attnum)) AS column_types, pg_size_pretty(pg_relation_size(c.oid)) AS index_size, c.oid AS index_oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_am am ON am.oid = c.relam JOIN pg_index i ON i.indexrelid = c.oid JOIN pg_class t ON t.oid = i.indrelid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(i.indkey) WHERE am.amname = 'biscuit' AND c.relkind = 'i' GROUP BY n.nspname, c.relname, t.relname, i.indkey, c.oid ORDER BY n.nspname, c.relname; COMMENT ON VIEW biscuit_indexes_detailed IS 'Detailed view of Biscuit indexes showing all columns and their types for multi-column indexes'; -- ==================== USAGE EXAMPLES ==================== -- Example queries (commented out - for documentation) /* -- ==================== SINGLE-COLUMN EXAMPLES ==================== -- Basic index creation CREATE INDEX idx_username ON users USING biscuit(username); CREATE INDEX idx_email ON users USING biscuit(email); -- Case-insensitive index (use LOWER()) CREATE INDEX idx_username_lower ON users USING biscuit(LOWER(username)); -- Partial index (only active users) CREATE INDEX idx_active_users ON users USING biscuit(username) WHERE status = 'active'; -- Query examples that use the index SELECT * FROM users WHERE username LIKE 'john%'; -- Prefix SELECT * FROM users WHERE email LIKE '%@gmail.com'; -- Suffix SELECT * FROM users WHERE username LIKE '%admin%'; -- Contains SELECT * FROM users WHERE username LIKE 'user_1%5'; -- Complex -- ==================== MULTI-COLUMN EXAMPLES ==================== -- Multi-column indexes (NEW in v1.0!) CREATE INDEX idx_name_email ON users USING biscuit(first_name, last_name); CREATE INDEX idx_full_contact ON users USING biscuit(first_name, last_name, email); -- Mixed type multi-column index CREATE INDEX idx_user_profile ON user_profiles USING biscuit(username, age, city); -- Query multi-column indexes SELECT * FROM users WHERE first_name LIKE 'John%' AND last_name LIKE 'S%'; SELECT * FROM user_profiles WHERE username LIKE 'admin%' AND age::text LIKE '3%' AND city LIKE 'New%'; -- ==================== MAINTENANCE ==================== -- Get index statistics SELECT biscuit_index_stats('idx_username'::regclass::oid); -- View all Biscuit indexes SELECT * FROM biscuit_indexes; -- View detailed multi-column info SELECT * FROM biscuit_indexes_detailed; -- List indexes on a specific table SELECT * FROM biscuit_indexes WHERE table_name = 'users'; -- Force index usage for testing SET enable_seqscan = off; EXPLAIN ANALYZE SELECT * FROM users WHERE username LIKE '%test%'; SET enable_seqscan = on; -- Maintenance VACUUM ANALYZE users; -- Clean up tombstones REINDEX INDEX idx_username; -- Rebuild if needed -- ==================== TYPE CONVERSION EXAMPLES ==================== -- Biscuit automatically handles type conversion for common types: -- - TEXT, VARCHAR, CHAR (native) -- - INTEGER, BIGINT (converted to zero-padded sortable strings) -- - FLOAT, DOUBLE PRECISION (converted to scientific notation) -- - DATE, TIMESTAMP (converted to sortable integers) -- - BOOLEAN (converted to 't'/'f') -- Example: Mixed type index CREATE TABLE products ( id SERIAL, name TEXT, price NUMERIC, created_date DATE ); CREATE INDEX idx_product_search ON products USING biscuit(name, price, created_date); -- Query will work seamlessly SELECT * FROM products WHERE name LIKE 'Widget%' AND price::text LIKE '9%' AND created_date::text LIKE '2024%'; */ -- ==================== GRANT PERMISSIONS ==================== -- Grant execute on functions to public (read-only diagnostic function) GRANT EXECUTE ON FUNCTION biscuit_index_stats(oid) TO PUBLIC; -- ==================== VERSION INFO ==================== -- Store extension version information CREATE TABLE IF NOT EXISTS biscuit_version ( version text PRIMARY KEY, installed_at timestamptz DEFAULT now(), description text ); INSERT INTO biscuit_version (version, description) VALUES ('1.0', 'Initial release with full CRUD support, O(1) deletion, multi-column indexes, and mixed type support'); COMMENT ON TABLE biscuit_version IS 'Version history for the Biscuit IAM extension'; -- ==================== HELPFUL QUERIES ==================== -- Function to check multi-column capability CREATE FUNCTION biscuit_multicolumn_enabled() RETURNS boolean AS $$ SELECT amcanmulticol FROM pg_am WHERE amname = 'biscuit'; $$ LANGUAGE SQL STABLE; COMMENT ON FUNCTION biscuit_multicolumn_enabled() IS 'Returns true if multi-column indexing is enabled for Biscuit indexes';