@mastra/pg 0.2.10-alpha.3 → 0.2.10-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type { QueryResult } from '@mastra/core';
2
+ import * as pg from 'pg';
2
3
  import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
3
4
 
4
5
  import { PgVector } from '.';
@@ -1858,4 +1859,486 @@ describe('PgVector', () => {
1858
1859
  await vectorDB.deleteIndex(indexName);
1859
1860
  });
1860
1861
  });
1862
+
1863
+ describe('Schema Support', () => {
1864
+ const customSchema = 'mastra_test';
1865
+ let vectorDB: PgVector;
1866
+ let customSchemaVectorDB: PgVector;
1867
+
1868
+ beforeAll(async () => {
1869
+ // Initialize default vectorDB first
1870
+ vectorDB = new PgVector(connectionString);
1871
+
1872
+ // Create schema using the default vectorDB connection
1873
+ const client = await vectorDB['pool'].connect();
1874
+ try {
1875
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${customSchema}`);
1876
+ await client.query('COMMIT');
1877
+ } catch (e) {
1878
+ await client.query('ROLLBACK');
1879
+ throw e;
1880
+ } finally {
1881
+ client.release();
1882
+ }
1883
+
1884
+ // Now create the custom schema vectorDB instance
1885
+ customSchemaVectorDB = new PgVector({
1886
+ connectionString,
1887
+ schemaName: customSchema,
1888
+ });
1889
+ });
1890
+
1891
+ afterAll(async () => {
1892
+ // Clean up test tables and schema
1893
+ try {
1894
+ await customSchemaVectorDB.deleteIndex('schema_test_vectors');
1895
+ } catch {
1896
+ // Ignore errors if index doesn't exist
1897
+ }
1898
+
1899
+ // Drop schema using the default vectorDB connection
1900
+ const client = await vectorDB['pool'].connect();
1901
+ try {
1902
+ await client.query(`DROP SCHEMA IF EXISTS ${customSchema} CASCADE`);
1903
+ await client.query('COMMIT');
1904
+ } catch (e) {
1905
+ await client.query('ROLLBACK');
1906
+ throw e;
1907
+ } finally {
1908
+ client.release();
1909
+ }
1910
+
1911
+ // Disconnect in reverse order
1912
+ await customSchemaVectorDB.disconnect();
1913
+ await vectorDB.disconnect();
1914
+ });
1915
+
1916
+ describe('Constructor', () => {
1917
+ it('should accept connectionString directly', () => {
1918
+ const db = new PgVector(connectionString);
1919
+ expect(db).toBeInstanceOf(PgVector);
1920
+ });
1921
+
1922
+ it('should accept config object with connectionString', () => {
1923
+ const db = new PgVector({ connectionString });
1924
+ expect(db).toBeInstanceOf(PgVector);
1925
+ });
1926
+
1927
+ it('should accept config object with schema', () => {
1928
+ const db = new PgVector({ connectionString, schemaName: customSchema });
1929
+ expect(db).toBeInstanceOf(PgVector);
1930
+ });
1931
+ });
1932
+
1933
+ describe('Schema Operations', () => {
1934
+ const testIndexName = 'schema_test_vectors';
1935
+
1936
+ beforeEach(async () => {
1937
+ // Clean up any existing indexes
1938
+ try {
1939
+ await customSchemaVectorDB.deleteIndex(testIndexName);
1940
+ } catch {
1941
+ // Ignore if doesn't exist
1942
+ }
1943
+ try {
1944
+ await vectorDB.deleteIndex(testIndexName);
1945
+ } catch {
1946
+ // Ignore if doesn't exist
1947
+ }
1948
+ });
1949
+
1950
+ afterEach(async () => {
1951
+ // Clean up indexes after each test
1952
+ try {
1953
+ await customSchemaVectorDB.deleteIndex(testIndexName);
1954
+ } catch {
1955
+ // Ignore if doesn't exist
1956
+ }
1957
+ try {
1958
+ await vectorDB.deleteIndex(testIndexName);
1959
+ } catch {
1960
+ // Ignore if doesn't exist
1961
+ }
1962
+ });
1963
+
1964
+ it('should create and query index in custom schema', async () => {
1965
+ // Create index in custom schema
1966
+ await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
1967
+
1968
+ // Insert test vectors
1969
+ const vectors = [
1970
+ [1, 2, 3],
1971
+ [4, 5, 6],
1972
+ ];
1973
+ const metadata = [{ test: 'custom_schema_1' }, { test: 'custom_schema_2' }];
1974
+ await customSchemaVectorDB.upsert({ indexName: testIndexName, vectors, metadata });
1975
+
1976
+ // Query and verify results
1977
+ const results = await customSchemaVectorDB.query({
1978
+ indexName: testIndexName,
1979
+ queryVector: [1, 2, 3],
1980
+ topK: 2,
1981
+ });
1982
+ expect(results).toHaveLength(2);
1983
+ expect(results[0]?.metadata?.test).toMatch(/custom_schema_/);
1984
+
1985
+ // Verify table exists in correct schema
1986
+ const client = await customSchemaVectorDB['pool'].connect();
1987
+ try {
1988
+ const res = await client.query(
1989
+ `
1990
+ SELECT EXISTS (
1991
+ SELECT FROM information_schema.tables
1992
+ WHERE table_schema = $1
1993
+ AND table_name = $2
1994
+ )`,
1995
+ [customSchema, testIndexName],
1996
+ );
1997
+ expect(res.rows[0].exists).toBe(true);
1998
+ } finally {
1999
+ client.release();
2000
+ }
2001
+ });
2002
+
2003
+ it('should allow same index name in different schemas', async () => {
2004
+ // Create same index name in both schemas
2005
+ await vectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
2006
+ await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
2007
+
2008
+ // Insert different test data in each schema
2009
+ await vectorDB.upsert({
2010
+ indexName: testIndexName,
2011
+ vectors: [[1, 2, 3]],
2012
+ metadata: [{ test: 'default_schema' }],
2013
+ });
2014
+
2015
+ await customSchemaVectorDB.upsert({
2016
+ indexName: testIndexName,
2017
+ vectors: [[1, 2, 3]],
2018
+ metadata: [{ test: 'custom_schema' }],
2019
+ });
2020
+
2021
+ // Query both schemas and verify different results
2022
+ const defaultResults = await vectorDB.query({
2023
+ indexName: testIndexName,
2024
+ queryVector: [1, 2, 3],
2025
+ topK: 1,
2026
+ });
2027
+ const customResults = await customSchemaVectorDB.query({
2028
+ indexName: testIndexName,
2029
+ queryVector: [1, 2, 3],
2030
+ topK: 1,
2031
+ });
2032
+
2033
+ expect(defaultResults[0]?.metadata?.test).toBe('default_schema');
2034
+ expect(customResults[0]?.metadata?.test).toBe('custom_schema');
2035
+ });
2036
+
2037
+ it('should maintain schema separation for all operations', async () => {
2038
+ // Create index in custom schema
2039
+ await customSchemaVectorDB.createIndex({ indexName: testIndexName, dimension: 3 });
2040
+
2041
+ // Test index operations
2042
+ const stats = await customSchemaVectorDB.describeIndex(testIndexName);
2043
+ expect(stats.dimension).toBe(3);
2044
+
2045
+ // Test list operation
2046
+ const indexes = await customSchemaVectorDB.listIndexes();
2047
+ expect(indexes).toContain(testIndexName);
2048
+
2049
+ // Test update operation
2050
+ const vectors = [[7, 8, 9]];
2051
+ const metadata = [{ test: 'updated_in_custom_schema' }];
2052
+ const [id] = await customSchemaVectorDB.upsert({
2053
+ indexName: testIndexName,
2054
+ vectors,
2055
+ metadata,
2056
+ });
2057
+
2058
+ // Test delete operation
2059
+ await customSchemaVectorDB.deleteIndexById(testIndexName, id!);
2060
+
2061
+ // Verify deletion
2062
+ const results = await customSchemaVectorDB.query({
2063
+ indexName: testIndexName,
2064
+ queryVector: [7, 8, 9],
2065
+ topK: 1,
2066
+ });
2067
+ expect(results).toHaveLength(0);
2068
+ });
2069
+ });
2070
+ });
2071
+
2072
+ describe('Permission Handling', () => {
2073
+ const schemaRestrictedUser = 'mastra_schema_restricted';
2074
+ const vectorRestrictedUser = 'mastra_vector_restricted';
2075
+ const restrictedPassword = 'test123';
2076
+ const testSchema = 'test_schema';
2077
+
2078
+ const getConnectionString = (username: string) =>
2079
+ connectionString.replace(/(postgresql:\/\/)[^:]+:[^@]+@/, `$1${username}:${restrictedPassword}@`);
2080
+
2081
+ beforeAll(async () => {
2082
+ // First ensure the test schema doesn't exist from previous runs
2083
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2084
+ try {
2085
+ await adminClient.query('BEGIN');
2086
+
2087
+ // Drop the test schema if it exists from previous runs
2088
+ await adminClient.query(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
2089
+
2090
+ // Create schema restricted user with minimal permissions
2091
+ await adminClient.query(`
2092
+ DO $$
2093
+ BEGIN
2094
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${schemaRestrictedUser}') THEN
2095
+ CREATE USER ${schemaRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
2096
+ END IF;
2097
+ END
2098
+ $$;
2099
+ `);
2100
+
2101
+ // Grant only connect and usage to schema restricted user
2102
+ await adminClient.query(`
2103
+ REVOKE ALL ON DATABASE ${connectionString.split('/').pop()} FROM ${schemaRestrictedUser};
2104
+ GRANT CONNECT ON DATABASE ${connectionString.split('/').pop()} TO ${schemaRestrictedUser};
2105
+ REVOKE ALL ON SCHEMA public FROM ${schemaRestrictedUser};
2106
+ GRANT USAGE ON SCHEMA public TO ${schemaRestrictedUser};
2107
+ `);
2108
+
2109
+ // Create vector restricted user with table creation permissions
2110
+ await adminClient.query(`
2111
+ DO $$
2112
+ BEGIN
2113
+ IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${vectorRestrictedUser}') THEN
2114
+ CREATE USER ${vectorRestrictedUser} WITH PASSWORD '${restrictedPassword}' NOCREATEDB;
2115
+ END IF;
2116
+ END
2117
+ $$;
2118
+ `);
2119
+
2120
+ // Grant connect, usage, and create to vector restricted user
2121
+ await adminClient.query(`
2122
+ REVOKE ALL ON DATABASE ${connectionString.split('/').pop()} FROM ${vectorRestrictedUser};
2123
+ GRANT CONNECT ON DATABASE ${connectionString.split('/').pop()} TO ${vectorRestrictedUser};
2124
+ REVOKE ALL ON SCHEMA public FROM ${vectorRestrictedUser};
2125
+ GRANT USAGE, CREATE ON SCHEMA public TO ${vectorRestrictedUser};
2126
+ `);
2127
+
2128
+ await adminClient.query('COMMIT');
2129
+ } catch (e) {
2130
+ await adminClient.query('ROLLBACK');
2131
+ throw e;
2132
+ } finally {
2133
+ adminClient.release();
2134
+ }
2135
+ });
2136
+
2137
+ afterAll(async () => {
2138
+ // Clean up test users and any objects they own
2139
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2140
+ try {
2141
+ await adminClient.query('BEGIN');
2142
+
2143
+ // Helper function to drop user and their objects
2144
+ const dropUser = async username => {
2145
+ // First revoke all possible privileges and reassign objects
2146
+ await adminClient.query(
2147
+ `
2148
+ -- Handle object ownership (CASCADE is critical here)
2149
+ REASSIGN OWNED BY ${username} TO postgres;
2150
+ DROP OWNED BY ${username} CASCADE;
2151
+
2152
+ -- Finally drop the user
2153
+ DROP ROLE ${username};
2154
+ `,
2155
+ );
2156
+ };
2157
+
2158
+ // Drop both users
2159
+ await dropUser(vectorRestrictedUser);
2160
+ await dropUser(schemaRestrictedUser);
2161
+
2162
+ await adminClient.query('COMMIT');
2163
+ } catch (e) {
2164
+ await adminClient.query('ROLLBACK');
2165
+ throw e;
2166
+ } finally {
2167
+ adminClient.release();
2168
+ }
2169
+ });
2170
+
2171
+ describe('Schema Creation', () => {
2172
+ beforeEach(async () => {
2173
+ // Ensure schema doesn't exist before each test
2174
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2175
+ try {
2176
+ await adminClient.query('BEGIN');
2177
+ await adminClient.query(`DROP SCHEMA IF EXISTS ${testSchema} CASCADE`);
2178
+ await adminClient.query('COMMIT');
2179
+ } catch (e) {
2180
+ await adminClient.query('ROLLBACK');
2181
+ throw e;
2182
+ } finally {
2183
+ adminClient.release();
2184
+ }
2185
+ });
2186
+
2187
+ it('should fail when user lacks CREATE privilege', async () => {
2188
+ const restrictedDB = new PgVector({
2189
+ connectionString: getConnectionString(schemaRestrictedUser),
2190
+ schemaName: testSchema,
2191
+ });
2192
+
2193
+ // Test schema creation directly by accessing private method
2194
+ await expect(async () => {
2195
+ const client = await restrictedDB['pool'].connect();
2196
+ try {
2197
+ await restrictedDB['setupSchema'](client);
2198
+ } finally {
2199
+ client.release();
2200
+ }
2201
+ }).rejects.toThrow(`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`);
2202
+
2203
+ // Verify schema was not created
2204
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2205
+ try {
2206
+ const res = await adminClient.query(
2207
+ `SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
2208
+ [testSchema],
2209
+ );
2210
+ expect(res.rows[0].exists).toBe(false);
2211
+ } finally {
2212
+ adminClient.release();
2213
+ }
2214
+
2215
+ await restrictedDB.disconnect();
2216
+ });
2217
+
2218
+ it('should fail with schema creation error when creating index', async () => {
2219
+ const restrictedDB = new PgVector({
2220
+ connectionString: getConnectionString(schemaRestrictedUser),
2221
+ schemaName: testSchema,
2222
+ });
2223
+
2224
+ // This should fail with the schema creation error
2225
+ await expect(async () => {
2226
+ await restrictedDB.createIndex({ indexName: 'test', dimension: 3 });
2227
+ }).rejects.toThrow(`Unable to create schema "${testSchema}". This requires CREATE privilege on the database.`);
2228
+
2229
+ // Verify schema was not created
2230
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2231
+ try {
2232
+ const res = await adminClient.query(
2233
+ `SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)`,
2234
+ [testSchema],
2235
+ );
2236
+ expect(res.rows[0].exists).toBe(false);
2237
+ } finally {
2238
+ adminClient.release();
2239
+ }
2240
+
2241
+ await restrictedDB.disconnect();
2242
+ });
2243
+ });
2244
+
2245
+ describe('Vector Extension', () => {
2246
+ beforeEach(async () => {
2247
+ // Create test table and grant necessary permissions
2248
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2249
+ try {
2250
+ await adminClient.query('BEGIN');
2251
+
2252
+ // First install vector extension
2253
+ await adminClient.query('CREATE EXTENSION IF NOT EXISTS vector');
2254
+
2255
+ // Drop existing table if any
2256
+ await adminClient.query('DROP TABLE IF EXISTS test CASCADE');
2257
+
2258
+ // Create test table as admin
2259
+ await adminClient.query('CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, embedding vector(3))');
2260
+
2261
+ // Grant ALL permissions including index creation
2262
+ await adminClient.query(`
2263
+ GRANT ALL ON TABLE test TO ${vectorRestrictedUser};
2264
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${vectorRestrictedUser};
2265
+ ALTER TABLE test OWNER TO ${vectorRestrictedUser};
2266
+ `);
2267
+
2268
+ await adminClient.query('COMMIT');
2269
+ } catch (e) {
2270
+ await adminClient.query('ROLLBACK');
2271
+ throw e;
2272
+ } finally {
2273
+ adminClient.release();
2274
+ }
2275
+ });
2276
+
2277
+ afterEach(async () => {
2278
+ // Clean up test table
2279
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2280
+ try {
2281
+ await adminClient.query('BEGIN');
2282
+ await adminClient.query('DROP TABLE IF EXISTS test CASCADE');
2283
+ await adminClient.query('COMMIT');
2284
+ } catch (e) {
2285
+ await adminClient.query('ROLLBACK');
2286
+ throw e;
2287
+ } finally {
2288
+ adminClient.release();
2289
+ }
2290
+ });
2291
+
2292
+ it('should handle lack of superuser privileges gracefully', async () => {
2293
+ // First ensure vector extension is not installed
2294
+ const adminClient = await new pg.Pool({ connectionString }).connect();
2295
+ try {
2296
+ await adminClient.query('DROP EXTENSION IF EXISTS vector CASCADE');
2297
+ } finally {
2298
+ adminClient.release();
2299
+ }
2300
+
2301
+ const restrictedDB = new PgVector({
2302
+ connectionString: getConnectionString(vectorRestrictedUser),
2303
+ });
2304
+
2305
+ try {
2306
+ const warnSpy = vi.spyOn(restrictedDB['logger'], 'warn');
2307
+
2308
+ // Try to create index which will trigger vector extension installation attempt
2309
+ await expect(restrictedDB.createIndex({ indexName: 'test', dimension: 3 })).rejects.toThrow();
2310
+
2311
+ expect(warnSpy).toHaveBeenCalledWith(
2312
+ expect.stringContaining('Could not install vector extension. This requires superuser privileges'),
2313
+ );
2314
+
2315
+ warnSpy.mockRestore();
2316
+ } finally {
2317
+ // Ensure we wait for any pending operations before disconnecting
2318
+ await new Promise(resolve => setTimeout(resolve, 100));
2319
+ await restrictedDB.disconnect();
2320
+ }
2321
+ });
2322
+
2323
+ it('should continue if vector extension is already installed', async () => {
2324
+ const restrictedDB = new PgVector({
2325
+ connectionString: getConnectionString(vectorRestrictedUser),
2326
+ });
2327
+
2328
+ try {
2329
+ const debugSpy = vi.spyOn(restrictedDB['logger'], 'debug');
2330
+
2331
+ await restrictedDB.createIndex({ indexName: 'test', dimension: 3 });
2332
+
2333
+ expect(debugSpy).toHaveBeenCalledWith('Vector extension already installed, skipping installation');
2334
+
2335
+ debugSpy.mockRestore();
2336
+ } finally {
2337
+ // Ensure we wait for any pending operations before disconnecting
2338
+ await new Promise(resolve => setTimeout(resolve, 100));
2339
+ await restrictedDB.disconnect();
2340
+ }
2341
+ });
2342
+ });
2343
+ });
1861
2344
  });