diff --git a/schema/src/cwms/at_schema_tr.sql b/schema/src/cwms/at_schema_tr.sql index 95bff22d..6d01bc84 100644 --- a/schema/src/cwms/at_schema_tr.sql +++ b/schema/src/cwms/at_schema_tr.sql @@ -435,4 +435,7 @@ ALTER TABLE "&cwms_schema"."AT_TR_TEMPLATE" ADD ( CONSTRAINT at_tr_template_r02 FOREIGN KEY (ts_code_indep_1) REFERENCES "&cwms_schema"."AT_CWMS_TS_SPEC" (ts_code)) -/ \ No newline at end of file +/ + +@@./triggers/at_fcst_location_trig.sql +@@./triggers/at_fcst_time_series_trig.sql \ No newline at end of file diff --git a/schema/src/cwms/cwms_fcst_pkg.sql b/schema/src/cwms/cwms_fcst_pkg.sql index 69b62e39..77fb4086 100644 --- a/schema/src/cwms/cwms_fcst_pkg.sql +++ b/schema/src/cwms/cwms_fcst_pkg.sql @@ -3,6 +3,22 @@ create or replace package cwms_fcst * Routines for storing and retrieving forecast information. Replaces package cwms_forecast. */ as +/** + * Global flag to defer validation in triggers. + */ +g_defer_validation boolean := false; +/** + * Sets the global flag to defer validation in triggers. + * + * @param p_defer A flag ('T'/'F') specifying whether to defer validation. + */ +procedure set_defer_validation(p_defer in varchar2); +/** + * Performs validation of forecast specification time series against forecast locations. + * + * @param p_fcst_spec_code The UUID of the forecast specification to validate. + */ +procedure validate_fcst_spec(p_fcst_spec_code in varchar2); /** * Stores (inserts or updates) a forecast specification * @@ -12,7 +28,12 @@ as * AT_ENTITY table. * @param p_description A description of the forecast specification. If unspecified or NULL no description is used. * @param p_location_id The primary location associated with forecast specification (e.g., project, basin, control point). - * If unspecified or NULL no location will be associated. + * If unspecified or NULL no location will be associated. This location is assigned a sort order of -1. + * @param p_location_ids A list of location IDs that are stored for this forecast specification separated by newline + * characters ("\n"). These locations are in addition to p_location_id (if specified). + * @param p_sort_orders A list of sort orders associated with the location IDs in p_location_ids. If specified, must + * have the same number of elements as p_location_ids. If unspecified or NULL, the locations in + * p_location_ids will be assigned sort order of 1. Sort order -1 indicates a primary location. * @param p_timeseries_ids A list of time series IDs that are stored for this forecast specification separated by newline * characters ("\n") * If unspecified or NULL no time series will be associated with the forecast specification until @@ -32,6 +53,8 @@ procedure store_fcst_spec( p_entity_id in varchar2, p_description in varchar2 default null, p_location_id in varchar2 default null, + p_location_ids in clob default null, + p_sort_orders in clob default null, p_timeseries_ids in clob default null, p_fail_if_exists in varchar2 default 'T', p_ignore_nulls in varchar2 default 'T', @@ -215,7 +238,10 @@ function cat_fcst_spec_f( * * @param p_entity_id The agency/office that generates forecasts for this specification * @param p_desccription The description of the forecast specification - * @param p_location_id The primary location associated with the forecast specification + * @param p_location_id The location ID(s) associated with the forecast specification. If multiple locations exist, + * they are returned as a newline-separated list. + * @param p_sort_order The sort order(s) associated with the location ID(s). If multiple locations exist, + * they are returned as a newline-separated list. * @param p_timeseries_ids The time series stored for this forecast specification, sorted lexically and separated by newline * characters ("\n") * @param p_fcst_spec_id The "main name" of the forecast. Must be non-null @@ -226,7 +252,8 @@ function cat_fcst_spec_f( procedure retrieve_fcst_spec( p_entity_id out varchar2, p_description out varchar2, - p_location_id out varchar2, + p_location_id out nocopy clob, + p_sort_order out nocopy clob, p_timeseries_ids out nocopy clob, p_fcst_spec_id in varchar2, p_fcst_designator in varchar2 default null, @@ -722,6 +749,22 @@ procedure delete_fcst( p_issue_date_time in date, p_time_zone in varchar2 default 'UTC', p_office_id in varchar2 default null); +/** + * Returns the UUID (surrogate key) of a forecast specification. + * + * @param p_office_code The office that owns the forecast specification + * @param p_fcst_spec_id The forecast specification identifier + * @param p_fcst_designator The forecast designator + * @param p_error_if_null A flag ('T'/'F') specifying whether to raise an exception if the specification is not found + * + * @return The UUID (surrogate key) of the forecast specification + */ +function get_fcst_spec_code( + p_office_code in cwms_office.office_code%type, + p_fcst_spec_id in varchar2, + p_fcst_designator in varchar2, + p_error_if_null in varchar2) + return at_fcst_spec.fcst_spec_code%type; end; / diff --git a/schema/src/cwms/cwms_fcst_pkg_body.sql b/schema/src/cwms/cwms_fcst_pkg_body.sql index e4a5fbd6..ac41fdb4 100644 --- a/schema/src/cwms/cwms_fcst_pkg_body.sql +++ b/schema/src/cwms/cwms_fcst_pkg_body.sql @@ -1,6 +1,44 @@ create or replace package body cwms_fcst as -------------------------------------------------------------------------------- --- private function get_fcst_spec_code +-- procedure set_defer_validation +-------------------------------------------------------------------------------- +procedure set_defer_validation(p_defer in varchar2) is +begin + g_defer_validation := cwms_util.return_true_or_false(p_defer); +end; +-------------------------------------------------------------------------------- +-- procedure validate_fcst_spec +-------------------------------------------------------------------------------- +procedure validate_fcst_spec(p_fcst_spec_code in varchar2) is + l_count integer; +begin + if g_defer_validation then + return; + end if; + for rec in ( + select + ts.ts_code, + cwms_ts.get_ts_id(ts.ts_code) as ts_id + from + at_fcst_time_series fts, + at_cwms_ts_spec ts + where + fts.fcst_spec_code = p_fcst_spec_code + and ts.ts_code = fts.ts_code + and ts.location_code not in ( + select location_code + from at_fcst_location + where fcst_spec_code = p_fcst_spec_code + ) + ) loop + cwms_err.raise( + 'ERROR', + 'Time series ' || rec.ts_id || ' does not belong to any of the forecast locations for this specification.' + ); + end loop; +end; +-------------------------------------------------------------------------------- +-- function get_fcst_spec_code -------------------------------------------------------------------------------- function get_fcst_spec_code( p_office_code in cwms_office.office_code%type, @@ -32,7 +70,7 @@ begin end if; end; return l_fcst_spec_code; -end; +end get_fcst_spec_code; -------------------------------------------------------------------------------- -- private function get_fcst_inst_code -------------------------------------------------------------------------------- @@ -91,6 +129,8 @@ procedure store_fcst_spec( p_entity_id in varchar2, p_description in varchar2 default null, p_location_id in varchar2 default null, + p_location_ids in clob default null, + p_sort_orders in clob default null, p_timeseries_ids in clob default null, p_fail_if_exists in varchar2 default 'T', p_ignore_nulls in varchar2 default 'T', @@ -101,7 +141,16 @@ is l_ts_code at_cwms_ts_spec.ts_code%type; l_fail_if_exists boolean; l_ignore_nulls boolean; + type loc_rec_t is record ( + location_id varchar2(256), + sort_order number + ); + type loc_tab_t is table of loc_rec_t; + l_locations loc_tab_t := loc_tab_t(); + l_ids cwms_t_str_tab; + l_orders cwms_t_str_tab; begin + g_defer_validation := true; ------------------- -- sanity checks -- ------------------- @@ -164,23 +213,42 @@ begin ------------------------- -- handle the location -- ------------------------- - if p_location_id is null then + if p_location_id is null and p_location_ids is null then if not l_ignore_nulls then delete from at_fcst_location where fcst_spec_code = l_rec.fcst_spec_code; end if; else - l_location_code := cwms_loc.get_location_code(l_rec.office_code, p_location_id); - merge into - at_fcst_location a - using - (select l_rec.fcst_spec_code as fcst_spec_code, - l_location_code as location_code - from dual - ) b - on - (a.fcst_spec_code = b.fcst_spec_code and a.primary_location_code = b.location_code) - when not matched then - insert values (l_rec.fcst_spec_code, l_location_code); + if not l_ignore_nulls then + delete from at_fcst_location where fcst_spec_code = l_rec.fcst_spec_code; + end if; + if p_location_id is not null then + l_locations.extend; + l_locations(l_locations.last).location_id := p_location_id; + l_locations(l_locations.last).sort_order := -1; + end if; + if p_location_ids is not null then + l_ids := cwms_util.split_text(p_location_ids, chr(10)); + if p_sort_orders is not null then + l_orders := cwms_util.split_text(p_sort_orders, chr(10)); + if l_orders.count != l_ids.count then + cwms_err.raise('ERROR', 'p_location_ids and p_sort_orders must have the same number of elements'); + end if; + end if; + for i in 1..l_ids.count loop + l_locations.extend; + l_locations(l_locations.last).location_id := trim(l_ids(i)); + if l_orders is not null then + l_locations(l_locations.last).sort_order := to_number(trim(l_orders(i))); + else + l_locations(l_locations.last).sort_order := 1; + end if; + end loop; + end if; + for i in 1..l_locations.count loop + l_location_code := cwms_loc.get_location_code(l_rec.office_code, l_locations(i).location_id); + insert into at_fcst_location (fcst_spec_code, location_code, sort_order) + values (l_rec.fcst_spec_code, l_location_code, l_locations(i).sort_order); + end loop; end if; ---------------------------- -- handle the time series -- @@ -205,6 +273,12 @@ begin insert values (l_rec.fcst_spec_code, l_ts_code); end loop; end if; + validate_fcst_spec(l_rec.fcst_spec_code); + g_defer_validation := false; +exception + when others then + g_defer_validation := false; + raise; end store_fcst_spec; -------------------------------------------------------------------------------- -- procedure cat_fcst_spec @@ -243,7 +317,8 @@ begin -- 4 entity_id varchar2(32) -- 5 entity_name varchar2(32) -- 6 description varchar2(256) - -- 7 times_series_ids sys_refcursor 7.1 time_series_id varchar2(193) + -- 7 location_id varchar2(256) (primary) + -- 8 times_series_ids sys_refcursor 8.1 time_series_id varchar2(193) open p_cursor for select o.office_id, fs.fcst_spec_id, @@ -251,6 +326,15 @@ begin e.entity_id, e.entity_name, fs.description, + (select bl.base_location_id||substr('-', 1, length(pl.sub_location_id))||pl.sub_location_Id + from at_fcst_location fl, + at_base_location bl, + at_physical_location pl + where fl.fcst_spec_code = fs.fcst_spec_code + and fl.sort_order = -1 + and pl.location_code = fl.location_code + and bl.base_location_code = pl.base_location_code + ) as location_id, cursor (select ts.cwms_ts_id as time_series_id from at_cwms_ts_id ts, at_fcst_time_series ft @@ -260,7 +344,7 @@ begin from at_fcst_spec fs, cwms_office o, at_entity e - where o.office_code = fs.office_code + where o.office_code = fs.office_code and e.entity_code = fs.source_entity and upper(fs.fcst_spec_id) like upper(l_fcst_spec_id_mask) escape '\' and upper(nvl(fs.fcst_designator, '~')) like upper(nvl(l_fcst_designator_mask, '~')) escape '\' @@ -293,7 +377,8 @@ end cat_fcst_spec_f; procedure retrieve_fcst_spec( p_entity_id out varchar2, p_description out varchar2, - p_location_id out varchar2, + p_location_id out nocopy clob, + p_sort_order out nocopy clob, p_timeseries_ids out nocopy clob, p_fcst_spec_id in varchar2, p_fcst_designator in varchar2 default null, @@ -302,6 +387,8 @@ is l_office_code cwms_office.office_code%type; l_fcst_spec_code at_fcst_spec.fcst_spec_code%type; l_ts_ids str_tab_t; + l_loc_ids str_tab_t; + l_sort_orders cwms_t_str_tab; begin ------------------- -- sanity checks -- @@ -312,41 +399,56 @@ begin ----------------- l_office_code := cwms_util.get_office_code(p_office_id); l_fcst_spec_code := get_fcst_spec_code(l_office_code, p_fcst_spec_id, p_fcst_designator, 'T'); - ----------------------------------------------- - -- get the entity, desctiption, and location -- - ----------------------------------------------- + --------------------------------- + -- get the entity, description -- + --------------------------------- select - entity_id, - description, - location_id + e.entity_id, + fs.description into p_entity_id, - p_description, - p_location_id + p_description from - (select - fs.fcst_spec_code, - e.entity_id, - fs.description - from - at_fcst_spec fs, - at_entity e - where - fs.fcst_spec_code = l_fcst_spec_code - and e.entity_code = fs.source_entity - ) q1 - left outer join - (select - fl.fcst_spec_code, - bl.base_location_id||substr('-', 1, length(pl.sub_location_id))||pl.sub_location_Id as location_id - from - at_fcst_location fl, - at_base_location bl, - at_physical_location pl - where - pl.location_code = fl.primary_location_code - and bl.base_location_code = pl.base_location_code - ) q2 on q2.fcst_spec_code = q1.fcst_spec_code; + at_fcst_spec fs, + at_entity e + where + fs.fcst_spec_code = l_fcst_spec_code + and e.entity_code = fs.source_entity; + ------------------------ + -- get the location(s) -- + ------------------------ + select + bl.base_location_id||substr('-', 1, length(pl.sub_location_id))||pl.sub_location_Id as location_id, + to_char(fl.sort_order) + bulk collect into + l_loc_ids, + l_sort_orders + from + at_fcst_location fl, + at_base_location bl, + at_physical_location pl + where + fl.fcst_spec_code = l_fcst_spec_code + and pl.location_code = fl.location_code + and bl.base_location_code = pl.base_location_code + order by + fl.sort_order, + location_id; + + if l_loc_ids.count > 0 then + dbms_lob.createtemporary(p_location_id, true); + dbms_lob.open(p_location_id, dbms_lob.lob_readwrite); + dbms_lob.createtemporary(p_sort_order, true); + dbms_lob.open(p_sort_order, dbms_lob.lob_readwrite); + cwms_util.append(p_location_id, l_loc_ids(1)); + cwms_util.append(p_sort_order, l_sort_orders(1)); + for i in 2..l_loc_ids.count loop + cwms_util.append(p_location_id, chr(10)||l_loc_ids(i)); + cwms_util.append(p_sort_order, chr(10)||l_sort_orders(i)); + end loop; + dbms_lob.close(p_location_id); + dbms_lob.close(p_sort_order); + end if; ------------------------- -- get the time series -- ------------------------- diff --git a/schema/src/cwms/cwms_loc_pkg_body.sql b/schema/src/cwms/cwms_loc_pkg_body.sql index f4c203cb..ff61d901 100644 --- a/schema/src/cwms/cwms_loc_pkg_body.sql +++ b/schema/src/cwms/cwms_loc_pkg_body.sql @@ -2803,7 +2803,7 @@ AS -- AT_FCST_xxx delete from at_fcst_location - where primary_location_code in (select * from table (l_location_codes)); + where location_code in (select * from table (l_location_codes)); -- AT_FORECAST_xxx select clob_code bulk collect diff --git a/schema/src/cwms/tables/at_fcst_location.sql b/schema/src/cwms/tables/at_fcst_location.sql index e41b5305..a3cf02ed 100644 --- a/schema/src/cwms/tables/at_fcst_location.sql +++ b/schema/src/cwms/tables/at_fcst_location.sql @@ -1,13 +1,17 @@ create table at_fcst_location ( - fcst_spec_code varchar2(36) not null, - primary_location_code number(14) not null, - constraint at_fcst_location_pk primary key (fcst_spec_code, primary_location_code) using index, + fcst_spec_code varchar2(36) not null, + location_code number(14) not null, + sort_order number not null, + constraint at_fcst_location_pk primary key (fcst_spec_code, location_code) using index, constraint at_fcst_location_fk1 foreign key (fcst_spec_code) references at_fcst_spec (fcst_spec_code), - constraint at_fcst_location_fk2 foreign key (primary_location_code) references at_physical_location (location_code) + constraint at_fcst_location_fk2 foreign key (location_code) references at_physical_location (location_code) ) tablespace cwms_20at_data; -create unique index at_fcst_location_idx1 on at_fcst_location (primary_location_code, fcst_spec_code); +create unique index at_fcst_location_idx1 on at_fcst_location (location_code, fcst_spec_code); -comment on table at_fcst_location is 'Holds information on primary locations for forecasts'; +create unique index at_fcst_location_idx2 on at_fcst_location (case when sort_order = -1 then fcst_spec_code end, case when sort_order = -1 then sort_order end); + +comment on table at_fcst_location is 'Holds information on locations for forecasts'; comment on column at_fcst_location.fcst_spec_code is 'References forecast specification'; -comment on column at_fcst_location.primary_location_code is 'References primary location for forecast'; +comment on column at_fcst_location.location_code is 'References location for forecast'; +comment on column at_fcst_location.sort_order is 'The sort order for the location. -1 indicates primary location.'; diff --git a/schema/src/cwms/triggers/at_fcst_location_trig.sql b/schema/src/cwms/triggers/at_fcst_location_trig.sql new file mode 100644 index 00000000..c3417e9a --- /dev/null +++ b/schema/src/cwms/triggers/at_fcst_location_trig.sql @@ -0,0 +1,7 @@ +create or replace trigger at_fcst_location_trig + after delete on at_fcst_location + for each row +begin + cwms_fcst.validate_fcst_spec(:old.fcst_spec_code); +end; +/ diff --git a/schema/src/cwms/triggers/at_fcst_time_series_trig.sql b/schema/src/cwms/triggers/at_fcst_time_series_trig.sql new file mode 100644 index 00000000..d979cc3b --- /dev/null +++ b/schema/src/cwms/triggers/at_fcst_time_series_trig.sql @@ -0,0 +1,9 @@ +create or replace trigger at_fcst_time_series_trig + after insert or update on at_fcst_time_series + for each row +begin + if not cwms_fcst.g_defer_validation then + cwms_fcst.validate_fcst_spec(:new.fcst_spec_code); + end if; +end; +/ diff --git a/schema/src/cwms/views/av_fcst_location.sql b/schema/src/cwms/views/av_fcst_location.sql index b21a12e1..6b4c41d3 100644 --- a/schema/src/cwms/views/av_fcst_location.sql +++ b/schema/src/cwms/views/av_fcst_location.sql @@ -6,10 +6,12 @@ insert into at_clob values (cwms_seq.nextval, 53, '/VIEWDOCS/AV_FCST_LOCATION', * @field office_id The office that owns the forecast * @field fcst_spec_id "Main name" of the forecast specification * @field fcst_designator "Sub-name" of the forecast specification, if any - * @field location_id The text ID of the primary location for this specification + * @field location_id The text ID of the location for this specification * @field office_code Numerical code of office that owns specification * @field fcst_spec_code UUID of specification - * @field location code Numerical code of the location + * @field location_code Numerical code of the location + * @field sort_order The sort order of the location + * @field is_primary 'T' if the location is the primary location for the specification, else 'F' */ '); create or replace view av_fcst_location ( @@ -19,7 +21,9 @@ create or replace view av_fcst_location ( location_id, office_code, fcst_spec_code, - location_code) + location_code, + sort_order, + is_primary) as select o.office_id, fs.fcst_spec_id, @@ -27,7 +31,9 @@ select o.office_id, bl.base_location_id||substr('-',1,length(pl.sub_location_id))||pl.sub_location_id as location_id, o.office_code, fs.fcst_spec_code, - fl.primary_location_code + fl.location_code, + fl.sort_order, + case when fl.sort_order = -1 then 'T' else 'F' end as is_primary from at_fcst_spec fs, at_fcst_location fl, cwms_office o, @@ -35,7 +41,7 @@ select o.office_id, at_base_location bl where o.office_code = fs.office_code and fs.fcst_spec_code = fl.fcst_spec_code - and fl.primary_location_code = pl.location_code + and fl.location_code = pl.location_code and bl.base_location_code = pl.base_location_code; grant select on av_fcst_location to cwms_user; diff --git a/schema/src/test/test_cwms_fcst.sql b/schema/src/test/test_cwms_fcst.sql index 56a6eb37..4797b798 100644 --- a/schema/src/test/test_cwms_fcst.sql +++ b/schema/src/test/test_cwms_fcst.sql @@ -12,6 +12,12 @@ procedure test_fcst_spec_ops; procedure test_fcst_inst_ops; --%test(Test forecast info uppercase keys and uniqueness) procedure test_fcst_info_uniqueness; +--%test(Test forecast location ordering and primary location concepts) +procedure test_fcst_loc_ordering; +--%test(Test timeseries validation for forecast specification) +procedure test_fcst_ts_validation; +--%test(Test updating primary location without deleting others (reordering)) +procedure test_fcst_loc_reordering; procedure setup; procedure teardown; @@ -80,6 +86,34 @@ begin exception when location_id_not_found then null; end; + begin + cwms_fcst.delete_fcst_spec( + p_fcst_spec_id => 'TestSpecInfo', + p_fcst_designator => 'TestDesigInfo', + p_delete_action => cwms_util.delete_all, + p_office_id => c_office_id); + exception + when item_does_not_exist then null; + end; + for rec in ( + select column_value as location_id + from table(cwms_util.split_text( + c_location_id || '1' || chr(10) || + c_location_id || '2' || chr(10) || + c_location_id || '3' || chr(10) || + c_location_id || 'R1' || chr(10) || + c_location_id || 'R2' || chr(10) || + c_location_id || 'R3' || chr(10) || + c_location_id || 'A' || chr(10) || + c_location_id || 'B', + chr(10))) + ) loop + begin + cwms_loc.delete_location(rec.location_id, cwms_util.delete_all, c_office_id); + exception + when location_id_not_found then null; + end; + end loop; commit; end teardown; -------------------------------------------------------------------------------- @@ -90,7 +124,7 @@ is l_result clob; begin if p_input is null then return null; end if; - for c in (select column_value as rec from (cwms_util.split_text(p_input, chr(10))) order by 1) loop + for c in (select column_value as rec from table(cwms_util.split_text(p_input, chr(10))) order by 1) loop l_result := l_result||chr(10)||c.rec; end loop; return substr(l_result, 1); @@ -164,7 +198,8 @@ is l_description at_fcst_spec.description%type; l_description_out at_fcst_spec.description%type; l_location_id at_cwms_ts_id.location_id%type; - l_location_id_out at_cwms_ts_id.location_id%type; + l_location_id_out clob; + l_sort_order_out clob; l_timeseries_ids clob; l_timeseries_ids_out clob; l_timeseries_id at_cwms_ts_id.cwms_ts_id%type; @@ -291,13 +326,17 @@ begin and fcst_designator = c_fcst_designator; if has_location = 1 then ut.expect(l_count).to_equal(1); - select location_id - into l_location_id_out - from cwms_v_fcst_location - where office_id = c_office_id - and fcst_spec_id = c_fcst_spec_id - and fcst_designator = c_fcst_designator; - ut.expect(l_location_id_out).to_equal(c_location_id); + for rec in (select * + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + ) + loop + ut.expect(rec.location_id).to_equal(c_location_id); + ut.expect(rec.sort_order).to_equal(-1); + ut.expect(rec.is_primary).to_equal('T'); + end loop; else ut.expect(l_count).to_equal(0); end if; @@ -310,13 +349,17 @@ begin and fcst_designator is null; if has_location = 1 then ut.expect(l_count).to_equal(1); - select location_id - into l_location_id_out - from cwms_v_fcst_location - where office_id = c_office_id - and fcst_spec_id = c_fcst_spec_id - and fcst_designator is null; - ut.expect(l_location_id_out).to_equal(c_location_id); + for rec in (select * + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator is null + ) + loop + ut.expect(rec.location_id).to_equal(c_location_id); + ut.expect(rec.sort_order).to_equal(-1); + ut.expect(rec.is_primary).to_equal('T'); + end loop; else ut.expect(l_count).to_equal(0); end if; @@ -422,6 +465,7 @@ begin p_entity_id => l_entity_id_out, p_description => l_description_out, p_location_id => l_location_id_out, + p_sort_order => l_sort_order_out, p_timeseries_ids => l_timeseries_ids_out, p_fcst_spec_id => c_fcst_spec_id, p_fcst_designator => l_fcst_designator, @@ -905,6 +949,7 @@ begin filename => 'fcst.txt', media_type => 'text/plain', quality_code => 0, + description => l_file_description, the_blob => clob_to_blob(l_file_contents)), p_fail_if_exists => 'T' , p_office_id => c_office_id); @@ -1080,6 +1125,322 @@ begin p_office_id => c_office_id); end test_fcst_info_uniqueness; +--------------------------------------------------------------------------------- +-- procedure test_fcst_loc_ordering +--------------------------------------------------------------------------------- +procedure test_fcst_loc_ordering +is + l_loc1 constant varchar2(32) := c_location_id || '1'; + l_loc2 constant varchar2(32) := c_location_id || '2'; + l_loc3 constant varchar2(32) := c_location_id || '3'; + l_loc_ids clob; + l_sort_orders clob; + l_loc_ids_out clob; + l_sort_orders_out clob; + l_crsr sys_refcursor; + l_office_id cwms_office.office_id%type; + l_fcst_spec_id at_fcst_spec.fcst_spec_id%type; + l_fcst_designator at_fcst_spec.fcst_designator%type; + l_entity_id at_entity.entity_id%type; + l_entity_name at_entity.entity_name%type; + l_description at_fcst_spec.description%type; + l_location_id_out at_cwms_ts_id.location_id%type; + l_tsid_crsr sys_refcursor; + l_timeseries_ids clob; +begin + -- 1. Setup extra locations + cwms_loc.store_location(l_loc1, null, 'T', c_office_id); + cwms_loc.store_location(l_loc2, null, 'T', c_office_id); + cwms_loc.store_location(l_loc3, null, 'T', c_office_id); + + -- 2. Test primary location uniqueness constraint (-1) + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_id => l_loc1, -- sets sort_order = -1 + p_office_id => c_office_id); + + -- Direct-table uniqueness validation intentionally omitted here because it + -- depends on non-public cwms_fcst helper routines. The public API ordering and + -- primary-location behavior is exercised below through store/retrieve/catalog + -- operations. + + -- 3. Test storing various sort orders (including 0 as a valid non-primary order) + l_loc_ids := l_loc1 || chr(10) || l_loc2 || chr(10) || l_loc3; + l_sort_orders := '-1' || chr(10) || '0' || chr(10) || '1'; + + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_ids => l_loc_ids, + p_sort_orders => l_sort_orders, + p_fail_if_exists => 'F', + p_office_id => c_office_id); + + -- 4. Test retrieval + cwms_fcst.retrieve_fcst_spec( + p_location_id => l_loc_ids_out, + p_sort_order => l_sort_orders_out, + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_office_id => c_office_id, + p_entity_id => l_entity_id, -- adding these to match signature + p_description => l_description, -- although they are OUT parameters + p_timeseries_ids => l_timeseries_ids); -- need to make sure variables match types + + ut.expect(l_loc_ids_out).to_be_like('%'||l_loc1||'%'||l_loc2||'%'||l_loc3||'%'); + ut.expect(l_sort_orders_out).to_be_like('%-1%0%1%'); + + -- 5. Test catalog reflects primary location correctly + cwms_fcst.cat_fcst_spec( + p_cursor => l_crsr, + p_fcst_spec_id_mask => c_fcst_spec_id, + p_office_id_mask => c_office_id); + + fetch l_crsr + into l_office_id, + l_fcst_spec_id, + l_fcst_designator, + l_entity_id, + l_entity_name, + l_description, + l_tsid_crsr; + close l_crsr; + + select location_id + into l_location_id_out + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and sort_order = -1; + + ut.expect(l_location_id_out).to_equal(l_loc1); -- The one with -1 + + -- 6. Cleanup + cwms_fcst.delete_fcst_spec(c_fcst_spec_id, c_fcst_designator, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc1, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc2, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc3, cwms_util.delete_all, c_office_id); + +end test_fcst_loc_ordering; +--------------------------------------------------------------------------------- +-- procedure test_fcst_loc_reordering +--------------------------------------------------------------------------------- +procedure test_fcst_loc_reordering +is + l_count binary_integer; + l_loc1 constant varchar2(32) := c_location_id || 'R1'; + l_loc2 constant varchar2(32) := c_location_id || 'R2'; + l_loc3 constant varchar2(32) := c_location_id || 'R3'; + l_loc_ids clob; + l_sort_orders clob; + l_loc_ids_out clob; + l_sort_orders_out clob; + l_crsr sys_refcursor; + l_office_id cwms_office.office_id%type; + l_fcst_spec_id at_fcst_spec.fcst_spec_id%type; + l_fcst_designator at_fcst_spec.fcst_designator%type; + l_entity_id at_entity.entity_id%type; + l_entity_name at_entity.entity_name%type; + l_description at_fcst_spec.description%type; + l_location_id_out at_cwms_ts_id.location_id%type; + l_tsid_crsr sys_refcursor; + l_timeseries_ids clob; +begin + -- 1. Setup locations + cwms_loc.store_location(l_loc1, null, 'T', c_office_id); + cwms_loc.store_location(l_loc2, null, 'T', c_office_id); + cwms_loc.store_location(l_loc3, null, 'T', c_office_id); + + -- 2. Initial store: Loc1 primary, Loc2 and Loc3 secondary + l_loc_ids := l_loc1 || chr(10) || l_loc2 || chr(10) || l_loc3; + l_sort_orders := '-1' || chr(10) || '0' || chr(10) || '1'; + + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_ids => l_loc_ids, + p_sort_orders => l_sort_orders, + p_office_id => c_office_id); + + -- Verify initial primary + cwms_fcst.cat_fcst_spec( + p_cursor => l_crsr, + p_fcst_spec_id_mask => c_fcst_spec_id, + p_office_id_mask => c_office_id); + fetch l_crsr + into l_office_id, + l_fcst_spec_id, + l_fcst_designator, + l_entity_id, + l_entity_name, + l_description, + l_tsid_crsr; + close l_crsr; + + select location_id + into l_location_id_out + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and sort_order = -1; + + ut.expect(l_location_id_out).to_equal(l_loc1); + + -- 3. Reorder: Promote Loc2 to Primary (-1), demote Loc1 to 0, Loc3 stays 1 + -- We use p_ignore_nulls => 'F' to replace the existing mapping + l_loc_ids := l_loc1 || chr(10) || l_loc2 || chr(10) || l_loc3; + l_sort_orders := '0' || chr(10) || '-1' || chr(10) || '1'; + + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_ids => l_loc_ids, + p_sort_orders => l_sort_orders, + p_ignore_nulls => 'F', + p_fail_if_exists => 'F', + p_office_id => c_office_id); + + -- 4. Verify new primary + cwms_fcst.retrieve_fcst_spec( + p_location_id => l_loc_ids_out, + p_sort_order => l_sort_orders_out, + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_office_id => c_office_id, + p_entity_id => l_entity_id, + p_description => l_description, + p_timeseries_ids => l_timeseries_ids); + + -- Note: ordering in output might vary, but we expect Loc2 to have -1 + -- The retrieve_fcst_spec implementation returns them in order of at_fcst_location table + -- which depends on how it was inserted/indexed. + ut.expect(l_loc_ids_out).to_be_like('%'||l_loc1||'%'||l_loc2||'%'||l_loc3||'%'); + + select sort_order + into l_count + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and location_id = l_loc1; + + ut.expect(l_count).to_equal(0); + + select sort_order + into l_count + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and location_id = l_loc2; + + ut.expect(l_count).to_equal(-1); + + select sort_order + into l_count + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and location_id = l_loc3; + + ut.expect(l_count).to_equal(1); + + cwms_fcst.cat_fcst_spec( + p_cursor => l_crsr, + p_fcst_spec_id_mask => c_fcst_spec_id, + p_office_id_mask => c_office_id); + fetch l_crsr + into l_office_id, + l_fcst_spec_id, + l_fcst_designator, + l_entity_id, + l_entity_name, + l_description, + l_tsid_crsr; + close l_crsr; + + select location_id + into l_location_id_out + from cwms_v_fcst_location + where office_id = c_office_id + and fcst_spec_id = c_fcst_spec_id + and fcst_designator = c_fcst_designator + and sort_order = -1; + + ut.expect(l_location_id_out).to_equal(l_loc2); -- New primary + + -- 5. Cleanup + cwms_fcst.delete_fcst_spec(c_fcst_spec_id, c_fcst_designator, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc1, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc2, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc3, cwms_util.delete_all, c_office_id); + +end test_fcst_loc_reordering; +--------------------------------------------------------------------------------- +-- procedure test_fcst_ts_validation +--------------------------------------------------------------------------------- +procedure test_fcst_ts_validation +is + l_loc1 constant varchar2(32) := c_location_id || 'A'; + l_loc2 constant varchar2(32) := c_location_id || 'B'; + l_tsid1 constant varchar2(256) := l_loc1 || '.Stage.Inst.1Hour.0.Fcst'; + l_tsid2 constant varchar2(256) := l_loc2 || '.Stage.Inst.1Hour.0.Fcst'; +begin + -- Setup + cwms_loc.store_location(l_loc1, null, 'T', c_office_id); + cwms_loc.store_location(l_loc2, null, 'T', c_office_id); + cwms_ts.zstore_ts(l_tsid1, 'ft', cwms_t_ztsv_array(), cwms_util.replace_all, cwms_util.non_versioned, c_office_id); + cwms_ts.zstore_ts(l_tsid2, 'ft', cwms_t_ztsv_array(), cwms_util.replace_all, cwms_util.non_versioned, c_office_id); + + -- 1. Success case: tsid location is in fcst locations + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_id => l_loc1, + p_timeseries_ids => l_tsid1, + p_office_id => c_office_id); + + -- 2. Failure case: tsid location (l_loc2) is NOT in fcst locations (only l_loc1 is) + begin + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_id => l_loc1, + p_timeseries_ids => l_tsid2, + p_fail_if_exists => 'F', + p_office_id => c_office_id); + cwms_err.raise('ERROR', 'Should have failed because l_loc2 is not a forecast location'); + exception + when others then + ut.expect(dbms_utility.format_error_stack).to_be_like('%Time series '||l_tsid2||' does not belong to any%'); + end; + + -- 3. Success case: Adding both locations + cwms_fcst.store_fcst_spec( + p_fcst_spec_id => c_fcst_spec_id, + p_fcst_designator => c_fcst_designator, + p_entity_id => 'CE'||c_office_id, + p_location_ids => l_loc1 || chr(10) || l_loc2, + p_sort_orders => '-1' || chr(10) || '1', + p_timeseries_ids => l_tsid2, + p_fail_if_exists => 'F', + p_office_id => c_office_id); + + -- 4. Cleanup + cwms_fcst.delete_fcst_spec(c_fcst_spec_id, c_fcst_designator, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc1, cwms_util.delete_all, c_office_id); + cwms_loc.delete_location(l_loc2, cwms_util.delete_all, c_office_id); +end test_fcst_ts_validation; end test_cwms_fcst; / diff --git a/schema/src/test/test_lrts_updates.sql b/schema/src/test/test_lrts_updates.sql index 86bcf51f..91a0aa22 100644 --- a/schema/src/test/test_lrts_updates.sql +++ b/schema/src/test/test_lrts_updates.sql @@ -1094,10 +1094,11 @@ begin delete from at_fcst_spec where fcst_spec_code = l_fcst_spec_code; - insert into at_fcst_spec values (l_fcst_spec_code, l_cwms_office_code, - 'TEST_SPEC876', 'designator', 1, 'description'); + insert into at_fcst_spec (fcst_spec_code, office_code, fcst_spec_id, fcst_designator, source_entity, description) + values (l_fcst_spec_code, l_cwms_office_code, 'TEST_SPEC876', 'designator', 1, 'description'); - insert into at_fcst_time_series values (l_fcst_spec_code, l_cwms_ts_code); + insert into at_fcst_time_series (fcst_spec_code, ts_code) + values (l_fcst_spec_code, l_cwms_ts_code); select cwms_ts_id into l_fcst_spec_tsid @@ -1152,12 +1153,13 @@ begin delete from at_fcst_spec where fcst_spec_code = l_fcst_spec_code; - insert into at_fcst_spec values (l_fcst_spec_code, l_cwms_office_code, - l_fcst_spec_id, 'designator', 1, 'description'); + insert into at_fcst_spec (fcst_spec_code, office_code, fcst_spec_id, fcst_designator, source_entity, description) + values (l_fcst_spec_code, l_cwms_office_code, l_fcst_spec_id, 'designator', 1, 'description'); - insert into at_fcst_location values (l_fcst_spec_code, l_location_code); + insert into at_fcst_location values (l_fcst_spec_code, l_location_code, -1); - insert into at_fcst_time_series values (l_fcst_spec_code, l_cwms_ts_code); + insert into at_fcst_time_series (fcst_spec_code, ts_code) + values (l_fcst_spec_code, l_cwms_ts_code); cwms_fcst.retrieve_fcst_spec( p_entity_id => l_entity_id,