@@ -1115,7 +1115,7 @@ def index_creation_tester(cur: psycopg.Cursor, vectorizer_id: int) -> None:
1115
1115
1116
1116
# insert 5 rows into the target
1117
1117
cur .execute (f"""
1118
- insert into { vectorizer .config [' destination' ][ ' target_schema' ]} .{ vectorizer .config [' destination' ][ ' target_table' ]}
1118
+ insert into { vectorizer .config [" destination" ][ " target_schema" ]} .{ vectorizer .config [" destination" ][ " target_table" ]}
1119
1119
( embedding_uuid
1120
1120
, id
1121
1121
, chunk_seq
@@ -1151,7 +1151,7 @@ def index_creation_tester(cur: psycopg.Cursor, vectorizer_id: int) -> None:
1151
1151
1152
1152
# insert 5 rows into the target
1153
1153
cur .execute (f"""
1154
- insert into { vectorizer .config [' destination' ][ ' target_schema' ]} .{ vectorizer .config [' destination' ][ ' target_table' ]}
1154
+ insert into { vectorizer .config [" destination" ][ " target_schema" ]} .{ vectorizer .config [" destination" ][ " target_table" ]}
1155
1155
( embedding_uuid
1156
1156
, id
1157
1157
, chunk_seq
@@ -1416,7 +1416,7 @@ def test_index_create_concurrency():
1416
1416
1417
1417
# insert 10 rows into the target
1418
1418
cur .execute (f"""
1419
- insert into { vectorizer .config [' destination' ][ ' target_schema' ]} .{ vectorizer .config [' destination' ][ ' target_table' ]}
1419
+ insert into { vectorizer .config [" destination" ][ " target_schema" ]} .{ vectorizer .config [" destination" ][ " target_table" ]}
1420
1420
( embedding_uuid
1421
1421
, id
1422
1422
, chunk_seq
@@ -1808,7 +1808,7 @@ def test_grant_to_public():
1808
1808
cur .execute (f"""
1809
1809
select has_table_privilege
1810
1810
( 'public'
1811
- , '{ vectorizer .config [' destination' ][ ' target_schema' ]} .{ vectorizer .config [' destination' ][ ' target_table' ]} '
1811
+ , '{ vectorizer .config [" destination" ][ " target_schema" ]} .{ vectorizer .config [" destination" ][ " target_table" ]} '
1812
1812
, 'select'
1813
1813
)""" )
1814
1814
assert cur .fetchone ()[0 ]
@@ -2222,3 +2222,164 @@ def test_install_library_before_ai_extension():
2222
2222
with psycopg .connect (db_url ("test" )) as con :
2223
2223
with con .cursor () as cur :
2224
2224
cur .execute ("create extension ai cascade" )
2225
+
2226
+
2227
+ @pytest .mark .skipif (
2228
+ os .getenv ("PG_MAJOR" ) == "15" , reason = "extension does not support pg15"
2229
+ )
2230
+ def test_set_scheduling ():
2231
+ with psycopg .connect (db_url ("test" )) as con :
2232
+ with con .cursor () as cur :
2233
+ cur .execute ("create extension ai cascade" )
2234
+
2235
+ with psycopg .connect (
2236
+ db_url ("postgres" ), autocommit = True , row_factory = namedtuple_row
2237
+ ) as con :
2238
+ with con .cursor () as cur :
2239
+ cur .execute ("create extension if not exists timescaledb" )
2240
+ cur .execute ("select to_regrole('bob') is null" )
2241
+ if cur .fetchone ()[0 ] is True :
2242
+ cur .execute ("create user bob" )
2243
+ cur .execute ("select to_regrole('adelaide') is null" )
2244
+ if cur .fetchone ()[0 ] is True :
2245
+ cur .execute ("create user adelaide" )
2246
+ with psycopg .connect (
2247
+ db_url ("test" ), autocommit = True , row_factory = namedtuple_row
2248
+ ) as con :
2249
+ con .add_notice_handler (detailed_notice_handler )
2250
+ with con .cursor () as cur :
2251
+ cur .execute ("drop schema if exists website cascade" )
2252
+ cur .execute ("create schema website" )
2253
+ cur .execute ("drop table if exists website.blog" )
2254
+ cur .execute ("""
2255
+ create table website.blog
2256
+ ( id int not null generated always as identity
2257
+ , title text not null
2258
+ , published timestamptz
2259
+ , body text not null
2260
+ , drop_me text
2261
+ , primary key (title, published)
2262
+ )
2263
+ """ )
2264
+ cur .execute (
2265
+ """grant select, insert, update, delete on website.blog to bob, adelaide"""
2266
+ )
2267
+ cur .execute ("""grant usage on schema website to adelaide""" )
2268
+ cur .execute ("""
2269
+ insert into website.blog(title, published, body)
2270
+ values
2271
+ ('how to cook a hot dog', '2024-01-06'::timestamptz, 'put it on a hot grill')
2272
+ , ('how to make a sandwich', '2023-01-06'::timestamptz, 'put a slice of meat between two pieces of bread')
2273
+ , ('how to make stir fry', '2022-01-06'::timestamptz, 'pick up the phone and order takeout')
2274
+ """ )
2275
+
2276
+ # drop the drop_me column
2277
+ cur .execute ("alter table website.blog drop column drop_me" )
2278
+
2279
+ # create a vectorizer for the blog table
2280
+ # language=PostgreSQL
2281
+ cur .execute ("""
2282
+ select ai.create_vectorizer
2283
+ ( 'website.blog'::regclass
2284
+ , loading => ai.loading_column('body')
2285
+ , embedding=>ai.embedding_openai('text-embedding-3-small', 768)
2286
+ , chunking=>ai.chunking_character_text_splitter(128, 10)
2287
+ , formatting=>ai.formatting_python_template('title: $title published: $published $chunk')
2288
+ , scheduling=>ai.scheduling_timescaledb
2289
+ ( interval '5m'
2290
+ , initial_start=>'2050-01-06'::timestamptz
2291
+ , timezone=>'America/Chicago'
2292
+ )
2293
+ , grant_to=>ai.grant_to('bob', 'fernando') -- bob is good. fernando doesn't exist. don't grant to adelaide
2294
+ );
2295
+ """ )
2296
+ vectorizer_id = cur .fetchone ()[0 ]
2297
+
2298
+ # check the vectorizer that was created
2299
+ cur .execute (
2300
+ """
2301
+ select jsonb_pretty(to_jsonb(x) #- array['config', 'version'])
2302
+ from ai.vectorizer x
2303
+ where x.id = %s
2304
+ """ ,
2305
+ (vectorizer_id ,),
2306
+ )
2307
+ actual = json .dumps (json .loads (cur .fetchone ()[0 ]), sort_keys = True , indent = 2 )
2308
+ expected = json .dumps (json .loads (VECTORIZER_ROW ), sort_keys = True , indent = 2 )
2309
+ assert actual == expected
2310
+
2311
+ # get timescaledb job's job_id
2312
+ cur .execute (
2313
+ """
2314
+ select (x.config->'scheduling'->>'job_id')::int
2315
+ from ai.vectorizer x
2316
+ where x.id = %s
2317
+ """ ,
2318
+ (vectorizer_id ,),
2319
+ )
2320
+ current_job_id = cur .fetchone ()[0 ]
2321
+
2322
+ # check the timescaledb job that was created
2323
+ cur .execute (
2324
+ """
2325
+ select j.schedule_interval = interval '5m'
2326
+ and j.proc_schema = 'ai'
2327
+ and j.proc_name = '_vectorizer_job'
2328
+ and j.scheduled = true
2329
+ and j.fixed_schedule = true
2330
+ as is_ok
2331
+ from timescaledb_information.jobs j
2332
+ where j.job_id = %s
2333
+ """ ,
2334
+ (current_job_id ,),
2335
+ )
2336
+ actual = cur .fetchone ()[0 ]
2337
+ assert actual is True
2338
+
2339
+ cur .execute (
2340
+ """
2341
+ select ai.set_scheduling
2342
+ ( %s
2343
+ , scheduling=>ai.scheduling_timescaledb
2344
+ ( interval '30m'
2345
+ , initial_start=>'2050-01-06'::timestamptz
2346
+ , timezone=>'America/Chicago'
2347
+ )
2348
+ , indexing=>ai.indexing_hnsw()
2349
+ )
2350
+ """ ,
2351
+ (vectorizer_id ,),
2352
+ )
2353
+
2354
+ # check the timescaledb old job that was deleted
2355
+ cur .execute (
2356
+ "select exists (select from timescaledb_information.jobs j where j.job_id = %s)" ,
2357
+ (current_job_id ,),
2358
+ )
2359
+ exists = cur .fetchone ()[0 ]
2360
+ assert not exists
2361
+
2362
+ cur .execute (
2363
+ "select config from ai.vectorizer where id = %s" , (vectorizer_id ,)
2364
+ )
2365
+ config = cur .fetchone ()[0 ]
2366
+ assert config ["scheduling" ]["schedule_interval" ] == "00:30:00"
2367
+ assert config ["indexing" ]["implementation" ] == "hnsw"
2368
+ job_id = config ["scheduling" ]["job_id" ]
2369
+ assert job_id != current_job_id
2370
+
2371
+ cur .execute (
2372
+ """
2373
+ select j.schedule_interval = interval '30m'
2374
+ and j.proc_schema = 'ai'
2375
+ and j.proc_name = '_vectorizer_job'
2376
+ and j.scheduled = true
2377
+ and j.fixed_schedule = true
2378
+ as is_ok
2379
+ from timescaledb_information.jobs j
2380
+ where j.job_id = %s
2381
+ """ ,
2382
+ (job_id ,),
2383
+ )
2384
+ actual = cur .fetchone ()[0 ]
2385
+ assert actual is True
0 commit comments