-- md_upsert.sql
--
-- PURPOSE
--	A set of execsql scripts to check data in a staging table, or
--	a set of staging tables, and then update and insert rows of a base table
--	or base tables from the staging table(s) of the same name.
--
--	This script contains code specific to MariaDB/MySQL.
--
-- HOW TO USE THESE SCRIPTS
--	In the following steps, "call" means to use an execsql "execute script"
--	metacommand.  Script names are displayed in uppercase to distinguish
--	them below, but execsql is not case-sensitive.
--
--	The simplest usage is:
--		1. Call STAGED_TO_LOAD to create and initially populate a table
--			with the names of staging tables to be loaded, and initial
--			control variables used by other scripts.
--		2. Call LOAD_STAGING to perform QA checks for erroneous nulls,
--			duplicated primary keys, and missing foreign keys, and to
--			load the data using update and insert statements if there were
--			no QA errors.
--
--	The control table that is produced in step 1 above can be edited to
--	add a list of columns to exclude from the update, or to change the
--	defaults for display of changed data.  See the header notes for the
--	STAGED_TO_LOAD script.
--
--	The processes of performing QA checks and performing the upsert operations
--	can be further broken down into individual steps.  See Note #1 below
--	for the other scripts that can be used to carry out these steps.
--
-- NOTES
--	1. The scripts contained in this file that are intended to be called
--		directly by the user are:
--			STAGED_TO_LOAD	: Initialize a control table to load multiple tables.
--			LOAD_STAGING	: Perform all QA checks and load data from all staging tables.
--			QA_ALL			: Perform null and foreign key checks on all staging tables.
--			UPSERT_ALL		: Load data from all staging tables.
--			NULLQA_ONE		: Perform null column checks on one staging table.
--			PKQA_ONE		: Perform primary key checks on one staging table.
--			FKQA_ONE		: Perform foreign key checks on one staging table.
--			UPSERT_ONE		: Load data from one staging table.
--		This file contains other scripts that are intended to be used
--		only by one of the scripts listed above, and not called
--		directly by the user.
--	2. These scripts query the information schema to obtain
--		the information needed to perform the checks and changes.
--	3. These scripts create and delete tables in the active database.
--		Because MariaDB/MySQL does not (currently) support temporary
--		views, or views on temporary tables, most of the views and
--		tables that are created are potentially visible to other
--		database operations.  See the header notes for invdividual
--		execsql scripts for names of the tables and views used.
--	4. These scripts take arguments that control their functions.  They
--		also use global variables pertinent to logging, if they are
--		defined.  The logging-control variables are global because
--		they may also be used by other code that uses these scripts,
--		and some of that code may be older and only recognize global
--		variables rather than script arguments; logging is intended
--		to be compatible with them.
--	5. The control table that is used to drive the loading of multiple
--		staging tables will be updated by the QA scripts with information
--		about any QA failures.  This information consists of a list of
--		the names of the columns or constraints that failed the check,
--		with the number of failing rows in paretheses following the name.
--	6. The control table that is used to drive the loading of multiple
--		staging tables will be updated by the upsert operation with 
--		a count of the number of rows of the base table that are updated,
--		and a count of the number of rows that were inserted into the
--		base table.
--	7. All of these scripts assume that schema, table, and column
--		names need not be quoted.
--	8. These scripts create temporary tables and views.  All of thes
--		have prefixes of "ups_".  Scripts that include this file
--		should not use this prefix to avoid possible conflicts.
--
-- COPYRIGHT AND LICENSE
--	Copyright (c) 2019, R. Dreas Nielsen
--	This program is free software: you can redistribute it and/or modify it
--	under the terms of the GNU General Public License as published by the
--	Free Software Foundation, either version 3 of the License, or (at your
--	option) any later version. This program is distributed in the hope that
--	it will be useful, but WITHOUT ANY WARRANTY; without even the implied
--	warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--	GNU General Public License for more details. The GNU General Public
--	License is available at http://www.gnu.org/licenses/.

-- AUTHORS
--	Dreas Nielsen (RDN)
--	Elizabeth Shea (ES)
--
-- VERSION
--	2.0.0
--
-- HISTORY
--	 Date		 Remarks
--	----------	-----------------------------------------------------
--	2019-01-15	Began first component scripts.  RDN.
--	2019-01-30	Integrated separate scripts into this one file and
--				began modifications to set parameters and arguments.  RDN.
--	2019-01-31	Editing.  RDN.
--	2019-02-01	Completed revisions to set parameters and arguments.
--				Added scripts "staged_to_load", "upsert_all", and
--				"load_staging".
--				Added and modified documentation.  RDN.
--	2019-02-02	Debugging edits.  RDN.
--	2019-02-03	Edits from 2019-02-03 pg version for MariaDB/MySQL
--				compatibility.  RDN.
--	2019-02-05	Edits for MariaDB: conversion of staging schema to
--				a staging prefix.  RDN.
--	2019-02-14	Added where clause conditions for schema name equal
--				to the current database to prevent multiple null
--				checks when there are multiple databases with the
--				same table and column names.  RDN.
--	2019-02-15	Modified all internal table and view names to use a
--				prefix of "ups_".  RDN.
--	2019-02-16	Modified to include in the list of ordered tables all
--				tables that are not part of any foreign key relationship.
--				Modified to use an 'exclude_null_checks' setting.  RDN.
--	2019-02-17	Corrected the check to display changes in UPSERT_ONE.  RDN.
--	2019-02-21	Modifed QA_ALL_NULLLOOP and QA_ALL_FKLOOP to use local
--				variables and the new outer scope referent prefix for
--				the return values.  RDN.
--	2019-03-02	Changed to use 'autocommit with commit'.  Modified
--				recursive CTE to order dependencies to protect against
--				multi-table cycles.  RDN.
--	2019-03-02	Modified to show the number of rows with each invalid
--				foreign key value, in both the GUI display and the
--				logfile.  RDN.
--	2019-03-03	Added the 'do_commit' argument to LOAD_STAGING.  RDN.
--	2019-03-09	Added FKQA_ONE with related changes from SS version
--				by ES.  Modified addition of solo tables to the set
--				of ordered tables per ES revisions.  Other fixes
--				following ES changes to pg_upsert.sql.  Revised
--				recursive CTE for FK ordering to eliminate
--				hangs on referential cycles of >2 tables.  RDN.
--	2019-03-15	Added VALIDATE_ONE and calls to it.  RDN.
--	2019-03-24	Added constraints to PKQA_ONE to limit to the
--				current database.  RDN.
--	2019-06-08	Began addition of PK updating scripts.  Complete
--				through QA check 8. RDN.
--	2019-06-16	Nominally complete revisions of PK updating scripts. RDN.
--	2019-06-23	Completed debugging of PK updating scripts.  RDN.
-- ==================================================================



-- ################################################################
--			Script VALIDATE_ONE
-- ===============================================================
--
-- Utility script to validate both base and staging versions of one table.
-- Halts script processing if either of the tables are not present.
--
-- Input parameters:
--		stage_pfx		: The prefix for staging tables.
--		table_name  	: The name of the table.
--		script          : The name of the script in which the
--							schemas and table were referenced 
--							(for error reporting).
--		script_line		: The script line in which they were referenced
--							(for error reporting).
-- ===============================================================
-- !x! BEGIN SCRIPT VALIDATE_ONE with parameters (stage_pfx, table, script, script_line)

-- Initialize the strings used to compile error information
-- !x! sub_empty ~err_info
-- !x! sub_empty ~error_list


-- !x! if(table_exists(ups_invl_table))
	drop table if exists ups_invl_table cascade;
-- !x! endif
create table ups_invl_table
select
	group_concat(concat(tt.table_name, ' (', tt.schema_type, ')') order by tt.table_name separator ';') as schema_table
from
	(
	select	
		'base' as schema_type,
		'!!#table!!' as table_name
	union
	select
		
		'staging' as schema_type,
		'!!#stage_pfx!!!!#table!!' as table_name
	) as tt
	left join information_schema.tables as iss on tt.table_name=iss.table_name
where	
	iss.table_name is null
	and iss.table_schema = '!!$DB_NAME!!'
having count(*) > 0
;

-- !x! if(hasrows(ups_invl_table))
	-- !x! subdata ~err_info ups_invl_table
	-- !x! sub ~error_list Non-existent table: !!~err_info!!	
-- !x! endif



-- Halt script if any schemas or tables were found to be invalid
-- !x! if(not is_null("!!~error_list!!"))

	-- !x! sub ~error_message ERROR - INVALID OBJECT IN SCRIPT ARGUMENT
	
	-- !x! if(sub_defined(log_changes))
	-- !x! andif(is_true(!!log_changes!!))
		-- !x! write "==================================================================" to !!logfile!!
		-- !x! write "!!$current_time!! -- !!~error_message!!" to !!logfile!!
		-- !x! write "Script: !!#script!!; Line: !!#script_line!!" to !!logfile!!
		-- !x! write "!!~error_list!!" to !!logfile!!
	-- !x! endif
	
	-- !x! sub_append ~error_message Script: !!#script!!; Line: !!#script_line!!  -- !x! sub_append ~error_message !!~error_list!!  -- !x! halt message "!!~error_message!!"
	
-- !x! endif

-- !x! if(table_exists(ups_invl_table))
	drop table if exists ups_invl_table cascade;
-- !x! endif

-- !x! END SCRIPT
-- ####################  End of VALIDATE_ONE  #####################
-- ################################################################



-- ################################################################
--			Script VALIDATE_CONTROL
-- ===============================================================
--
-- Utility script to validate contents of control table against about
--	base and staging schema
--
-- Required input arguments:
--		stage_pfx		: The name of the staging schema.
--		control_table  	: The name of a temporary table as created by the
--							script STAGED_TO_LOAD.
--		script          : The name of the script in which the
--							schemas and control table were referenced 
--							(for error reporting).
--		script_line		: The script line in which they were referenced
--							(for error reporting).
-- ===============================================================
-- !x! BEGIN SCRIPT VALIDATE_CONTROL with parameters (stage_pfx, control_table, script, script_line)

-- Initialize the strings used to compile error information
-- !x! sub_empty ~err_info
-- !x! sub_empty ~error_list

-- !x! if(table_exists(ups_validate_control))
	drop table if exists ups_validate_control cascade;
-- !x! endif
create table ups_validate_control
select
	table_name, 
	False as base_exists,
	False as staging_exists
from !!#control_table!!
;

-- Update the validation table
update ups_validate_control as vc, information_schema.tables as bt
set vc.base_exists = True
where
	vc.table_name=bt.table_name
	and bt.table_type='BASE TABLE'
	and bt.table_schema = '!!$DB_NAME!!'
;

update ups_validate_control as vc, information_schema.tables as st
set vc.staging_exists = True
where
	st.table_name= concat('!!#stage_pfx!!', vc.table_name)
	and st.table_type='BASE TABLE'
	and st.table_schema = '!!$DB_NAME!!'
;

-- !x! if(table_exists(ups_ctrl_invl_table))
	drop table if exists ups_ctrl_invl_table cascade;
-- !x! endif
create table ups_ctrl_invl_table
select
		group_concat(schema_table order by it.schema_table separator '; ') as schema_table
from
		(
		select table_name as schema_table
		from ups_validate_control
		where not base_exists
		union
		select concat('!!#stage_pfx!!', table_name) as schema_table
		from ups_validate_control
		where not staging_exists	
		) as it
having count(*) > 0
;

-- Any table is invalid
-- !x! if(hasrows(ups_ctrl_invl_table))
	-- !x! subdata ~err_info ups_validate_control
	-- !x! sub ~error_list Non-existent table(s): !!~err_info!!	
-- !x! endif


-- Halt script if any invalid objects found in control table
-- !x! if(not is_null("!!~error_list!!"))

	-- !x! sub ~error_message ERROR - INVALID OBJECTS IN CONTROL TABLE
	
	-- !x! if(sub_defined(log_changes))
	-- !x! andif(is_true(!!log_changes!!))
		-- !x! write "==================================================================" to !!logfile!!
		-- !x! write "!!$current_time!! -- !!~error_message!!" to !!logfile!!
		-- !x! write "Script: !!#script!!; Line: !!#script_line!!" to !!logfile!!
		-- !x! write "!!~error_list!!" to !!logfile!!
	-- !x! endif
	
	-- !x! sub_append ~error_message Script: !!#script!!; Line: !!#script_line!!
	-- !x! sub_append ~error_message !!~error_list!!
	-- !x! halt message "!!~error_message!!"
	
-- !x! endif
		
-- !x! if(table_exists(ups_validate_control))
	drop table if exists ups_validate_control cascade;
-- !x! endif
-- !x! if(table_exists(ups_ctrl_invl_table))
	drop table if exists ups_ctrl_invl_table cascade;
-- !x! endif

-- !x! END SCRIPT
-- ####################  End of VALIDATE_CONTROL  #################
-- ################################################################


-- ################################################################
--			Script STAGED_TO_LOAD
-- ===============================================================
--
-- Creates a table having the structure that is used to drive other
-- scripts that perform QA checks and the upsert operation on multiple
-- staging tables.
--
-- Input parameters:
--		control_table	: The name of the table to be created.
--							In MariaDB/MySQL, this must *not* be a
--							temporary table.
--		table_list		: A string of comma-separated table names,
--							identifying the staging tables to be
--							checked or loaded.
--
-- Columns in the table created:
--		table_name		: The name of a base table that has a
--							corresponding table in a staging schema
--							containing data to be used to modify
--							the base table.
--		exclude_cols	: Contains a comma-separated list of columns
--							in the base table that are not to be
--							updated from the staging table.  This is
--							uninitialized.
--		exclude_null_checks :
--						  Contains a comma-separated list of single-quoted
--							column names identifying column in the
--							staging table for which null checks
--							should not be performed.  This is
--							uninitialized.
--		display_changes	: A Boolean indicating whether the 'upsert'
--							operation should display the changes to
--							be made to the base table.  A separate
--							GUI display is used for updates and inserts.
--							Initialized to True.
--		display_final	: A Boolean indicating whether the 'upsert'
--							operation should display the entire base
--							table after updates and inserts have been
--							made.  Initialized to False.
--		null_errors		: Will contain a comma-separated list of
--							columns that are non-nullable in the base
--							table but that have nulls in the staging
--							table.  Initialized to null; may be filled
--							by the QA routines.
--		pk_errors		: Will contain a count of the number of distinct
--							primary key values having duplicate rows,
--							followed by the total row count for the
--							duplicated keys. Initialized to null; may
--							be filled by the QA routines.  
--		fk_errors		: Will contain a comma-separated list of
--							foreign-key constraint names that are
--							not met by data in the staging table.
--							Initialized to null; may be filled by the
--							QA routines.
--		rows_updated	: Will contain a count of the rows updated
--							in the table by the upsert operation.
--		rows_inserted	: Will contain a count of the rows inserted
--							in the table by the upsert operation.
--
-- Example:
--		-- !x! execute script staged_to_load with (control_table=stagingtables, table_list="tbla, tblb, tblc")
-- ===============================================================

-- !x! BEGIN SCRIPT STAGED_TO_LOAD with parameters (control_table, table_list)

drop table if exists !!#control_table!! cascade;

create table !!#control_table!! (
	table_name varchar(150) not null unique,
	exclude_cols varchar(255),
	exclude_null_checks varchar(255),
	display_changes varchar(3) not null default 'Yes',
	display_final varchar(3) not null default 'No',
	null_errors varchar(255),
	pk_errors varchar(255),
	fk_errors varchar(255),
	rows_updated integer,
	rows_inserted integer,
	constraint ck_chgyn check (display_changes in ('Yes', 'No')),
	constraint ck_finalyn check (display_final in ('Yes', 'No'))
);

insert into !!#control_table!!
	(table_name)
with recursive itemtable as (
		select
			trim(substring_index(data, ',', 1)) as table_name,
			right(data, length(data) - locate(',', data, 1)) as data
		from (select '!!#table_list!!' as data) as input
		union
		select
			trim(substring_index(data, ',', 1)) as table_name,
			right(data, length(data) - locate(',', data, 1)) as data
		from itemtable
	)
select table_name as item
from itemtable;


-- !x! END SCRIPT
-- ##################  End of STAGED_TO_LOAD  #####################
-- ################################################################



-- ################################################################
--			Script LOAD_STAGING
-- ===============================================================
--
-- Performs QA checks for nulls in non-null columns, for duplicated
-- primary key values, and for invalid foreign keys in a set of staging
-- tables to be loaded into base tables.  If there are failures in the
-- QA checks, loading is not attempted.  If the loading step is
-- carried out, it is done within a transaction.
--
-- The "null_errors" and "fk_errors" columns of the control table
-- will be updated to identify any errors that occur, so that this
-- information is available to the caller.
--
-- The "rows_updated" and "rows_inserted" columns of the control table
-- will be updated with counts of the number of rows affected by the
-- upsert operation for each table.
--
-- When the upsert operation updates the base table, all columns of the
-- base table that are also in the staging table are updated.  The
-- update operation does not test to see if column contents are different,
-- and so does not update only those values that are different.
--
-- Input parameters:
--		stage_pfg		: The prefix on staging tables that distinguish
--							them from the corresponding based table
--							(e.g., "stg_").
--		control_table	: The name of a table as created by the
--							script STAGED_TO_LOAD.
--		do_commit		: Whether or not the script should commit
--							the changes; should be 'Yes' or 'No'.
-- Global variables:
--		logfile			: The name of a log file to which error
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' to indicate whether
--							the SQL that is generated for each foreign
--							key check, and for each update and insert
--							statement, is written to the logfile.  Optional.
--		log_errors		: A value of 'Yes' or 'No' to indicate whether
--							foreign key errors are written to the logfile.
--							Optional.
--		log_changes		: A value of 'Yes' or 'No' indicating whether
--							the updated and inserted data should be
--							written to the logfile.  Optional.
--
-- Tables and views created or modified:
--		ups_qa_fails				: view
-- ===============================================================

-- !x! BEGIN SCRIPT LOAD_STAGING with parameters (stage_pfx, control_table, do_commit)

-- Clear the columns of return values from the control table, in case this control
-- table has been used previously.
update !!#control_table!!
set
	null_errors = null,
	pk_errors = null,
	fk_errors = null,
	rows_updated = null,
	rows_inserted = null
	;


-- Run null checks, PK checks, and FK checks.
-- !x! execute script QA_ALL with arguments (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)
-- !x! if(view_exists(ups_qa_fails))
	drop view if exists ups_qa_fails cascade;
-- !x! endif
create view ups_qa_fails as
select *
from !!#control_table!!
where null_errors is not null or pk_errors is not null or fk_errors is not null;
-- !x! if(not hasrows(ups_qa_fails))
	-- !x! sub ~preautocommit !!$autocommit_state!!
	-- !x! autocommit off
	-- !x! execute script UPSERT_ALL with arguments (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)
	-- !x! if(is_true(!!#do_commit!!))
		-- !x! autocommit on with commit
		-- !x! write "CHANGES COMMITTED."
		-- !x! if(sub_defined(log_changes))
		-- !x! andif(is_true(!!log_changes!!))
			-- !x! write "" to !!logfile!!
			-- !x! write "==================================================================" to !!logfile!!
			-- !x! write "!!$current_time!! -- CHANGES COMMITTED." to !!logfile!!
		-- !x! endif
	-- !x! else 
		-- !x! write "CHANGES NOT COMMITTED ('do_commit' argument = !!#do_commit!!)"
		-- !x! if(sub_defined(log_changes))
		-- !x! andif(is_true(!!log_changes!!))
			-- !x! write "" to !!logfile!!
			-- !x! write "==================================================================" to !!logfile!!
			-- !x! write "!!$current_time!! -- CHANGES NOT COMMITTED ('do_commit' argument = !!#do_commit!!)" to !!logfile!!
		-- !x! endif
	-- !x! endif
	-- !x! autocommit !!~preautocommit!!
-- !x! endif

-- Clean up.
drop view if exists ups_qa_fails cascade;

-- !x! END SCRIPT
-- ###################  End of LOAD_STAGING  ######################
-- ################################################################




-- ################################################################
--			Script NULLQA_ONE
-- ===============================================================
--
-- Checks that non-nullable columns are fully populated in a
-- staging table that is an image of the base table.
-- Reports any non-conforming columns to the console and optionally
-- to a log file.
--
-- Required input arguments:
--		stage_pfx		: The name of the staging schema.
--		table			: The table name--same for base and staging.
-- Optional input arguments:
--		exclude_null_checks : A comma-separated list of singly-quoted
--								column names to be excluded from null checks.
--
--	Output parameters:
--		error_list		: The name of the variable to receive a comma-
--							delimited list of the names of non-null
--							columns that contain nulls; each column name
--							will be followed by the number of rows with
--							nulls, in parentheses.
--
-- Global variables:
--		logfile			: The name of a log file to which error
--							messages will be written.  Optional.
--
-- Tables and views created or modified:
--		ups_nonnull_columns		: table
--		ups_next_column			: view
--		ups_null_error_list		: view
--		ups_qa_nonnull_col		: view
-- ===============================================================

-- !x! BEGIN SCRIPT NULLQA_ONE with parameters (stage_pfx, table, error_list)

-- Write an initial header to the logfile.
-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- Non-null QA checks on table !!#stage_pfx!!!!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Conducting non-null QA checks on table !!#stage_pfx!!!!#table!!"

-- Validate input table specifications.
-- !x! execute script validate_one with args (stage_pfx=!!#stage_pfx!!, table=!!#table!!, script=!!$CURRENT_SCRIPT!!, script_line=!!$SCRIPT_LINE!!)

-- Initialize the return value to empty (no foreign key errors)
-- !x! sub_empty !!#error_list!!

-- Create a table listing the columns of the base table that must
-- be non-null and that do not have a default expression.
-- Include a column for the number of rows with nulls in the staging table.
-- Include a 'processed' column for loop control.
-- !x! if(sub_defined(#exclude_null_checks))
	-- !x! sub ~omitnull and column_name not in (!!#exclude_null_checks!!)
-- !x! else
	-- !x! sub_empty ~omitnull
-- !x! endif
-- !x! if(table_exists(ups_nonnull_columns))
	drop table if exists ups_nonnull_columns cascade;
-- !x! endif
create table ups_nonnull_columns
select
	column_name,
	0 as null_rows,
	0 as processed
from
	information_schema.columns
where
	table_schema = '!!$DB_NAME!!'
	and table_name = '!!#table!!'
	and is_nullable = 'NO'
	and column_default is null
	!!~omitnull!!
	;

-- Create a view to select one column to process.
create or replace view ups_next_column as
select column_name
from ups_nonnull_columns
where processed = 0
limit 1;

-- Process all non-nullable columns.
-- !x! execute script nullqa_one_innerloop with (stage_pfx=!!#stage_pfx!!, table=!!#table!!)

-- Create the return value.
create or replace view ups_null_error_list as
select
	group_concat(concat(column_name, ' (', cast(null_rows as varchar(100)), ')') separator ', ') as null_errors
from
	ups_nonnull_columns
where
	coalesce(null_rows, 0) > 0;
-- !x! if(hasrows(ups_null_error_list))
	-- !x! subdata !!#error_list!! ups_null_error_list
-- !x! endif

-- Clean up.
drop view if exists ups_null_error_list;
drop view if exists ups_next_column cascade;
drop table if exists ups_nonnull_columns cascade;

-- !x! END SCRIPT
-- End of          NULLQA_ONE
-- ****************************************************************
-- ****************************************************************
--			Script NULLQA_ONE_INNERLOOP
-- ---------------------------------------------------------------
-- !x! BEGIN SCRIPT NULLQA_ONE_INNERLOOP with parameters (stage_pfx, table)

-- !x! if(hasrows(ups_next_column))
	-- !x! subdata ~column_name ups_next_column

	-- !x! if(sub_defined(logfile))
		-- !x! write "Checking column !!~column_name!!." to !!logfile!!
	-- !x! endif

	create or replace view ups_qa_nonnull_col as
	select nrows
	from (
		select count(*) as nrows
		from !!#stage_pfx!!!!#table!!
		where !!~column_name!! is null
		) as nullcount
	where nrows > 0
	limit 1;
	-- !x! if(hasrows(ups_qa_nonnull_col))
		-- !x! subdata ~nullrows ups_qa_nonnull_col
		-- !x! write "    Column !!~column_name!! has !!~nullrows!! nulls."
		-- !x! if(sub_defined(logfile))
			-- !x! write "    Column !!~column_name!! has !!~nullrows!! nulls." to !!logfile!!
		-- !x! endif
		update ups_nonnull_columns
		set null_rows = (select nrows from ups_qa_nonnull_col limit 1)
		where column_name = '!!~column_name!!';
	-- !x! endif

	-- Mark this constraint as processed.
	update ups_nonnull_columns
	set processed = 1
	where column_name = '!!~column_name!!';

	-- Loop.
	-- !x! execute script nullqa_one_innerloop with (stage_pfx=!!#stage_pfx!!, table=!!#table!!)

-- !x! endif

-- Clean up.
-- !x! if(view_exists(ups_qa_nonnull_col))
	drop view if exists ups_qa_nonnull_col;
-- !x! endif


-- !x! END SCRIPT
-- ###################  End of NULL_QA_ONE  #######################
-- ################################################################



-- ################################################################
--			Script PKQA_ONE
--
-- Check data in a staging table for violations of the primary key 
-- of the corresponding base table.
-- Reports any PK violations found to the console and optionally
-- to a log file.
--
-- Input parameters:
--		stage_pfx		: The name of the staging schema.
--		table			: The table name--same for base and staging.
--		display_errors	: A value of 'Yes' or 'No' to indicate whether
--							unrecognized values should be displayed
--							in a GUI.
--	Output parameters:
--		error_list		: The name of the variable to receive a count
--							of the total number of distinct PK values
--							having violations, followed by a count of
--							the total rows associated with each.
--
-- Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' to indicate whether
--							the SQL that is generated for each foreign
--							key check is written to the logfile.  Optional.
--		log_errors		: A value of 'Yes' or 'No' to indicate whether
--							foreign key errors are written to the logfile.
--							Optional.
--
-- Tables and views created or modified:
--		ups_primary_key_columns		: table
--		ups_pk_check				: table
--		ups_ercnt					: view
-- ===============================================================

-- !x! BEGIN SCRIPT PKQA_ONE with parameters (stage_pfx, table, display_errors, error_list)

-- Write an initial header to the logfile.
-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- Primary key QA checks on table !!#stage_pfx!!!!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Conducting primary key QA checks on table !!#stage_pfx!!!!#table!!"

-- Validate input table specifications.
-- !x! execute script validate_one with args (stage_pfx=!!#stage_pfx!!, table=!!#table!!, script=!!$CURRENT_SCRIPT!!, script_line=!!$SCRIPT_LINE!!)

-- Initialize the return value to False (no primary key errors)
-- !x! sub_empty !!#error_list!!

-- Create a table of primary key columns on this table
-- !x! if(table_exists(ups_primary_key_columns))
	drop table ups_primary_key_columns cascade;
-- !x! endif
create table ups_primary_key_columns
select k.constraint_name, k.column_name, k.ordinal_position
from information_schema.table_constraints as tc
inner join information_schema.key_column_usage as k
    on tc.constraint_type = 'PRIMARY KEY' 
    and tc.constraint_name = k.constraint_name
    and tc.constraint_catalog = k.constraint_catalog
    and tc.constraint_schema = k.constraint_schema
    and tc.table_schema = k.table_schema
    and tc.table_name = k.table_name
	and tc.constraint_name = k.constraint_name
where
	k.table_name = '!!#table!!'
	and tc.constraint_schema = '!!$db_name!!'
order by k.ordinal_position
;

-- !x! if(hasrows(ups_primary_key_columns))
	-- !x! subdata ~constraint_name ups_primary_key_columns
	
	-- !x! if(sub_defined(logfile))
		-- !x! write "Checking constraint !!~constraint_name!!." to !!logfile!!
	-- !x! endif
	
	-- Get a comma-delimited list of primary key columns to build SQL selection for duplicate keys
	-- !x! sub_empty ~pkcollist
	-- !x! if(view_exists(ups_pkcollist))
		drop view if exists ups_pkcollist cascade;
	-- !x! endif
	create view ups_pkcollist as
	select   group_concat(column_name separator ', ') as col_list
	from     ups_primary_key_columns
	order by ordinal_position;
	-- !x! subdata ~pkcollist ups_pkcollist
	
	-- Construct a query to test for duplicate values for pk columns.
	-- !x! sub            ~pk_check   select !!~pkcollist!!, count(*) as row_count
	-- !x! sub_append     ~pk_check   from !!#stage_pfx!!!!#table!! as s
	-- !x! sub_append     ~pk_check   group by !!~pkcollist!!
	-- !x! sub_append     ~pk_check   having count(*) > 1

	-- Write the SQL to the log file if requested.
	-- !x! if(sub_defined(logfile))
	-- !x! andif(sub_defined(log_sql))
	-- !x! andif(is_true(!!log_sql!!))
		-- !x! write "SQL for primary key check:" to !!logfile!!
		-- !x! write [!!~pk_check!!] to !!logfile!!
	-- !x! endif

	-- Run the check.
	-- !x! if(table_exists(ups_pk_check))
		drop table if exists ups_pk_check cascade;
	-- !x! endif
	create table ups_pk_check
	!!~pk_check!!;
	-- !x! if(hasrows(ups_pk_check))
		-- !x! write "    Duplicate key error on columns: !!~pkcollist!!."
		-- !x! if(view_exists(ups_ercnt))
			drop view if exists ups_ercnt cascade;
		-- !x! endif
		create view ups_ercnt
		select count(*) as errcnt, sum(row_count) as total_rows
		from ups_pk_check;
		-- !x! select_sub ups_ercnt
		-- !x! sub !!#error_list!! !!@errcnt!! duplicated key(s) (!!@total_rows!! rows)
		-- !x! if(sub_defined(logfile))
			-- !x! write "Duplicate primary key values in !!#table!!" to !!logfile!!
			-- !x! if(sub_defined(log_errors))
			-- !x! andif(is_true(!!log_errors!!))
				-- !x! export ups_pk_check append to !!logfile!! as txt
			-- !x! endif
		-- !x! endif
		-- !x! if(is_true(!!#display_errors!!))
			-- !x! prompt message "Primary key violations in !!stage_pfx!!!!#table!!" display ups_pk_check
		-- !x! endif
	-- !x! endif
-- !x! endif

-- Clean up.
drop view if exists ups_pkcollist cascade;
drop table if exists ups_primary_key_columns cascade;
drop table if exists ups_pk_check cascade;
-- !x! if(view_exists(ups_ercnt))
	drop view if exists ups_ercnt cascade;
-- !x! endif


-- !x! END SCRIPT
-- ####################  End of PKQA_ONE  ########################
-- ################################################################



-- ################################################################
--			Script FKQA_ONE
--
-- Checks foreign keys from a staging table against a base table
-- and, if it exists, another staging table that is an image of the
-- base table.
-- Reports any bad references found to the console and optionally
-- to a log file.
--
-- Input parameters:
--		stage_pfx		: The prefix for staging table names.
--		table			: The table name--same for base and staging.
--		display_errors	: A value of 'Yes' or 'No' to indicate whether
--							unrecognized values should be displayed
--							in a GUI.  If not defined, unrecognized
--							values are not displayed.
--	Output parameters:
--		error_list		: The name of the variable to receive a comma-
--							delimited list of the names of foreign key
--							constraints that are not met.
--
-- Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' to indicate whether
--							the SQL that is generated for each foreign
--							key check is written to the logfile.  Optional.
--		log_errors		: A value of 'Yes' or 'No' to indicate whether
--							foreign key errors are written to the logfile.
--							Optional.
--
-- Tables and views created or modified:
--		ups_foreign_key_columns		: table
--		ups_sel_fks					: table
--		ups_fk_constraints			: table
--		ups_next_constraint			: view
--		ups_error_list				: view
--		ups_one_fk					: table
--		ups_fk_joins				: view
--		ups_fk_check				: view
--		ups_ercnt					: view
-- ===============================================================

-- !x! BEGIN SCRIPT FKQA_ONE with parameters (stage_pfx, table, display_errors, error_list)

-- Write an initial header to the logfile.
-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- Foreign key QA checks on table !!#stage_pfx!!!!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Conducting foreign key QA checks on table !!#stage_pfx!!!!#table!!"

-- Validate input table specifications.
-- !x! execute script validate_one with args (stage_pfx=!!#stage_pfx!!, table=!!#table!!, script=!!$CURRENT_SCRIPT!!, script_line=!!$SCRIPT_LINE!!)

-- Initialize the return value to False (no foreign key errors)
-- !x! sub_empty !!#error_list!!


-- Create a table of *all* foreign key dependencies in this database.
-- Because this may be an expensive operation (in terms of time), the
-- table is not re-created if it already exists.  "Already exists"
-- means that a table with the expected name exists.  No check is
-- done to ensure that this table has the correct structure.  The
-- goal is to create the table of all foreign keys only once to
-- minimize the time required if QA checks are to be run on multiple
-- staging tables.
-- !x! if(not table_exists(ups_foreign_key_columns))
	create table ups_foreign_key_columns
	select
		rc.constraint_name,
		cu.table_schema,
		cu.table_name,
		cu.column_name,
		cu.ordinal_position,
		cu_uq.table_schema as uq_schema,
		cu_uq.table_name as uq_table,
		cu_uq.column_name as uq_column,
		cu_uq.ordinal_position as uq_position
	from
		(select constraint_catalog, constraint_schema, constraint_name,
			table_name,
			unique_constraint_catalog, unique_constraint_schema, unique_constraint_name,
            referenced_table_name
			from information_schema.referential_constraints
			where constraint_schema = '!!$db_name!!'
			) as rc
		inner join (select * from information_schema.table_constraints
			where constraint_type = 'FOREIGN KEY' and constraint_schema = '!!$db_name!!'
			) as tc
			on tc.constraint_catalog = rc.constraint_catalog
			and tc.constraint_schema = rc.constraint_schema
			and tc.constraint_name = rc.constraint_name
            and tc.table_name = rc.table_name
		inner join (select * from information_schema.table_constraints
			where constraint_type not in ('FOREIGN KEY', 'CHECK')
			and constraint_schema = '!!$db_name!!'
			) as tc_uq
			on tc_uq.constraint_catalog = rc.unique_constraint_catalog
			and tc_uq.constraint_schema = rc.unique_constraint_schema
			and tc_uq.constraint_name = rc.unique_constraint_name
            and tc_uq.table_name = rc.referenced_table_name
		inner join information_schema.key_column_usage as cu
			on cu.constraint_catalog = tc.constraint_catalog
			and cu.constraint_schema = tc.constraint_schema
			and cu.constraint_name = tc.constraint_name
            and cu.table_schema = tc.table_schema
			and cu.table_name = tc.table_name
		inner join information_schema.key_column_usage as cu_uq
			on cu_uq.constraint_catalog = tc_uq.constraint_catalog
			and cu_uq.constraint_schema = tc_uq.constraint_schema
			and cu_uq.constraint_name = tc_uq.constraint_name
			and cu_uq.table_schema = tc_uq.table_schema
            and cu_uq.table_name = tc_uq.table_name
			and cu_uq.ordinal_position = cu.ordinal_position
		;
-- !x! endif

-- Create a temporary table of just the foreign key relationships for the base
-- table corresponding to the staging table to check.
-- !x! if(table_exists(ups_sel_fks))
	drop table if exists ups_sel_fks cascade;
-- !x! endif
create table ups_sel_fks
select
	constraint_name, table_schema, table_name, column_name,
	ordinal_position,
	uq_schema, uq_table, uq_column
from
	ups_foreign_key_columns
where
	table_schema = '!!$DB_NAME!!'
	and table_name = '!!#table!!';

-- Create a table of all unique constraint names for
-- this table, with an integer column to be populated with the
-- number of rows failing the foreign key check, and a 'processed'
-- 	flag to control looping.
-- !x! if(table_exists(ups_fk_constraints))
	drop table if exists ups_fk_constraints cascade;
-- !x! endif
create table ups_fk_constraints
select distinct
	constraint_name,
	0 as fkerror_values,
	0 as processed
from ups_sel_fks;

-- Create a view to select one constraint to process.
create or replace view ups_next_constraint as
select constraint_name
from ups_fk_constraints
where processed = 0
limit 1;

-- Process all constraints: check every foreign key.
-- !x! execute script fk_qa_one_innerloop with (stage_pfx=!!#stage_pfx!!, table=!!#table!!, display_errors=!!#display_errors!!)

-- Create the return value.
create or replace view ups_error_list as
select
	group_concat(concat(constraint_name, ' (', fkerror_values, ')') separator ', ') as fk_errors
from
	ups_fk_constraints
where
	coalesce(fkerror_values, 0) > 0;
-- !x! if(hasrows(ups_error_list))
	-- !x! subdata !!#error_list!! ups_error_list
-- !x! endif

-- Clean up.
drop view if exists ups_error_list cascade;
drop view if exists ups_next_constraint cascade;
drop table if exists ups_fk_constraints cascade;
drop table if exists ups_sel_fks;

-- !x! END SCRIPT
-- End of          FK_QA_ONE
-- ****************************************************************
-- ****************************************************************
--			Script FK_QA_ONE_INNERLOOP
-- ----------------------------------------------------------------
-- !x! BEGIN SCRIPT FK_QA_ONE_INNERLOOP with parameters (stage_pfx, table, display_errors)

-- !x! if(hasrows(ups_next_constraint))
	-- !x! subdata constraint_name ups_next_constraint

	-- !x! if(sub_defined(logfile))
		-- !x! write "Checking constraint !!constraint_name!!." to !!logfile!!
	-- !x! endif

	-- !x! if(table_exists(ups_one_fk))
		drop table if exists ups_one_fk cascade;
	-- !x! endif
	create table ups_one_fk
	select column_name, ordinal_position, uq_schema, uq_table, uq_column
	from ups_sel_fks
	where constraint_name = '!!constraint_name!!';

	-- Get the unique table schema and name into data variables.
	-- !x! select_sub ups_one_fk

	-- Create join expressions from staging table (s) to unique table (u)
	-- and to staging table equivalent to unique table (su) (though we
	-- don't know yet if the latter exists).  Also create a 'where'
	-- condition to ensure that all columns being matched are non-null.
	-- Also create a comma-separated list of the columns being checked.
	create or replace view ups_fk_joins as
	select
		group_concat(concat('s.', column_name, ' = u.', uq_column) separator ' and ') as u_join,
		group_concat(concat('s.', column_name, ' = su.', uq_column) separator ' and ') as su_join,
		group_concat(concat('s.', column_name, ' is not null') separator ' and ') as s_not_null,
		group_concat(concat('s.', column_name) separator ', ') as s_checked
	from
		(select * from ups_one_fk order by ordinal_position) as fkcols;
	-- !x! select_sub ups_fk_joins
	
	-- Determine whether a staging-table equivalent of the unique table exists.
	-- !x! sub su_exists No
	-- !x! if(table_exists(!!#stage_pfx!!!!@uq_table!!))
		-- !x! sub su_exists Yes
	-- !x! endif

	-- Construct a query to test for missing unique values for fk columns.
	-- !x! sub            ~fk_check   select !!@s_checked!!, count(*) as row_count
	-- !x! sub_append     ~fk_check   from !!#stage_pfx!!!!#table!! as s
	-- !x! sub_append     ~fk_check   left join !!@uq_table!! as u on !!@u_join!!
	-- !x! if(is_true(!!su_exists!!))
		-- !x! sub_append ~fk_check   left join !!#stage_pfx!!!!@uq_table!! as su on !!@su_join!!
	-- !x! endif
	-- !x! sub_append     ~fk_check   where u.!!@uq_column!! is null
	-- !x! if(is_true(!!su_exists!!))
		-- !x! sub_append ~fk_check   and su.!!@uq_column!! is null
	-- !x! endif
	-- !x! sub_append     ~fk_check   and !!@s_not_null!!
	-- !x! sub_append     ~fk_check   group by !!@s_checked!!

	-- Write the SQL to the log file if requested.
	-- !x! if(sub_defined(logfile))
	-- !x! andif(sub_defined(log_sql))
	-- !x! andif(is_true(!!log_sql!!))
		-- !x! write "SQL for foreign key check:" to !!logfile!!
		-- !x! write [!!~fk_check!!] to !!logfile!!
	-- !x! endif

	-- Run the check.
	create or replace view ups_fk_check as !!~fk_check!!;
	-- !x! if(hasrows(ups_fk_check))
		-- !x! write "    Foreign key error referencing !!@uq_table!!."
		create or replace view ups_ercnt as select count(*) from ups_fk_check;
		-- !x! subdata ~errcnt ups_ercnt
		update ups_fk_constraints
		set fkerror_values = !!~errcnt!!
		where constraint_name = '!!constraint_name!!';
		-- !x! if(sub_defined(logfile))
			-- !x! write " Foreign key errors in !!#table!! referencing !!@uq_table!!" to !!logfile!!
			-- !x! if(sub_defined(log_errors))
			-- !x! andif(is_true(!!log_errors!!))
				-- !x! export ups_fk_check append to !!logfile!! as txt
			-- !x! endif
		-- !x! endif
		-- !x! if(is_true(!!#display_errors!!))
			-- !x! prompt message "Foreign key errors in !!#table!! referencing !!@uq_table!!" display ups_fk_check
		-- !x! endif
	-- !x! endif


	-- Mark this constraint as processed.
	update ups_fk_constraints
	set processed = 1
	where constraint_name = '!!constraint_name!!';

	-- Loop.
	-- !x! execute script fk_qa_one_innerloop with (stage_pfx=!!#stage_pfx!!, table=!!#table!!, display_errors=!!#display_errors!!)

-- !x! endif

-- Clean up.
-- !x! if(view_exists(ups_fk_check))
	drop view if exists ups_fk_check cascade; 
-- !x! endif
-- !x! if(view_exists(ups_fk_joins))
	drop view if exists ups_fk_joins cascade; 
-- !x! endif
-- !x! if(table_exists(ups_foreign_key_columns))
	drop table if exists ups_foreign_key_columns cascade;
-- !x! endif

-- Clean up.
-- !x! if(table_exists(ups_one_fk))
	drop table if exists ups_one_fk cascade;
-- !x! endif

-- !x! END SCRIPT
-- ####################  End of FK_QA_ONE  ########################
-- ################################################################




-- ################################################################
--			Script UPSERT_ONE
--
-- Adds data from a staging table to a base table, using UPDATE
-- and INSERT statements.  Displays data to be modified to the
-- user before any modifications are done.  Reports the changes
-- made to the console and optionally to a log file.
--
-- Input parameters:
--		stage_pfx		: The name of the staging schema.
--		table			: The table name--same for base and staging.
--		exclude_cols	: A comma-delimited list of single-quoted
--							column names identifying the columns
--							of the base table that are not to be
--							modified.  These may be autonumber
--							columns or columns filled by triggers.
--		display_changes	: A boolean variable indicating whether
--							or not the changes to be made to the 
--							base table should be displayed in a GUI.
--							Optional.  If not defined, the changes
--							will be defined.
--		display_final	: A boolean variable indicating whether or
--							not the base table should be displayed
--							after updates and inserts are completed.
--							Optional.  If not defined, the final
--							base table will not be displayed.
--		updcntvar		: The name of a substitution variable that
--							will be set to the number of rows updated.
--		inscntvar		: The name of a substitution variable that
--							will be set to the number of rows inserted.
--
--	Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' indicating whether
--							the update and insert statements should
--							also be written to the logfile.  Optional.
--		log_changes		: A value of 'Yes' or 'No' indicating whether
--							the updated and inserted data should be
--							written to the logfile.  Optional.
--
-- Tables and views created or modified:
--		ups_cols				: table
--		ups_pks					: table
--		ups_allcollist			: temporary view
--		ups_allbasecollist		: temporary view
--		ups_allstgcollist		: temporary view
--		ups_pkcollist			: temporary view
--		ups_joinexpr			: temporary view
--		ups_basematches			: temporary view
--		ups_stgmatches			: temporary view
--		ups_assexpr				: temporary view
--		ups_newrows				: temporary view
-- ===============================================================

-- !x! BEGIN SCRIPT UPSERT_ONE with parameters (stage_pfx, table, exclude_cols, display_changes, display_final, updcntvar, inscntvar)

-- Remove substitution variables that will contain the generated
-- update and insert statements so that the existence of valid
-- statements can be later tested based on the existence of these variables.
-- !x! rm_sub ~updatestmt
-- !x! rm_sub ~insertstmt

-- !x! sub ~do_updates Yes
-- !x! sub ~do_inserts Yes

-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- Performing upsert on table !!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Performing upsert on table !!#table!!"

-- Validate input table specifications.
-- !x! execute script validate_one with args (stage_pfx=!!#stage_pfx!!, table=!!#table!!, script=!!$CURRENT_SCRIPT!!, script_line=!!$SCRIPT_LINE!!)

-- Populate a (temporary) table with the names of the columns
-- in the base table that are to be updated from the staging table.
-- !x! if(is_null("!!#exclude_cols!!"))
	-- !x! sub_empty ~col_excl
-- !x! else
	-- !x! sub ~col_excl and column_name not in (!!#exclude_cols!!)
-- !x! endif
-- !x! if(table_exists(ups_cols))
	drop table if exists ups_cols cascade;
-- !x! endif
create table ups_cols
select column_name
from information_schema.columns
where
	table_name = '!!#stage_pfx!!!!#table!!'
	and table_schema = '!!$DB_NAME!!'
	!!~col_excl!!
order by ordinal_position;


-- Populate a (temporary) table with the names of the primary key
-- columns of the base table.
-- !x! if(table_exists(ups_pks))
	drop table if exists ups_pks cascade;
-- !x! endif
create table ups_pks
select k.column_name
from information_schema.table_constraints as tc
inner join information_schema.key_column_usage as k
    on tc.constraint_type = 'PRIMARY KEY' 
    and tc.constraint_name = k.constraint_name
    and tc.constraint_catalog = k.constraint_catalog
    and tc.constraint_schema = k.constraint_schema
    and tc.table_schema = k.table_schema
    and tc.table_name = k.table_name
	and tc.constraint_name = k.constraint_name
where
	k.table_name = '!!#table!!'
	and k.table_schema = '!!$DB_NAME!!'
order by k.ordinal_position;

-- Get all base table columns that are to be updated into a comma-delimited list.
-- !x! if(view_exists(ups_allcollist))
	drop view ups_allcollist cascade;
-- !x! endif
create view ups_allcollist as
select group_concat(column_name separator ', ')
from ups_cols;
-- !x! subdata ~allcollist ups_allcollist;


-- Get all base table columns that are to be updated into a comma-delimited list
-- with a "b." prefix.
-- !x! if(view_exists(ups_allbasecollist))
	drop view ups_allbasecollist cascade;
-- !x! endif
create view ups_allbasecollist as
select group_concat(concat('b.', column_name) separator ', ')
from ups_cols;
-- !x! subdata ~allbasecollist ups_allbasecollist;

-- Get all staging table column names for columns that are to be updated
-- into a comma-delimited list with an "s." prefix.
-- !x! if(view_exists(ups_allstgcollist))
	drop view ups_allstgcollist cascade;
-- !x! endif
create view ups_allstgcollist as
select group_concat(concat('s.', column_name) separator ', ')
from ups_cols;
-- !x! subdata ~allstgcollist ups_allstgcollist;


-- Get the primary key columns in a comma-delimited list.
-- !x! if(view_exists(ups_pkcollist))
	drop view ups_pkcollist cascade;
-- !x! endif
create view ups_pkcollist as
select group_concat(column_name separator ', ')
from ups_pks;
-- !x! subdata ~pkcollist ups_pkcollist;


-- Create a join expression for key columns of the base (b) and
-- staging (s) tables.
-- !x! if(view_exists(ups_joinexpr))
	drop view ups_joinexpr cascade;
-- !x! endif
create view ups_joinexpr as
select
	group_concat(concat('b.', column_name, ' = s.', column_name) separator ' and ')
from
	ups_pks;
-- !x! subdata ~joinexpr ups_joinexpr


-- Create a FROM clause for an inner join between base and staging
-- tables on the primary key column(s).
-- !x! sub ~fromclause FROM !!#table!! as b INNER JOIN !!#stage_pfx!!!!#table!! as s ON !!~joinexpr!!

-- Create SELECT queries to pull all columns with matching keys from both
-- base and staging tables.
-- !x! if(view_exists(ups_basematches))
	drop view ups_basematches cascade;
-- !x! endif
create view ups_basematches as select !!~allbasecollist!! !!~fromclause!!;

-- !x! if(view_exists(ups_stgmatches))
	drop view ups_stgmatches cascade;
-- !x! endif
create view ups_stgmatches as select !!~allstgcollist!! !!~fromclause!!;

-- Get non-key columns to be updated.
-- !x! if(view_exists(ups_nk))
	drop view if exists ups_nk cascade;
-- !x! endif
create view ups_nk as
select cols.column_name
from
	ups_cols as cols
	left join ups_pks as pks on pks.column_name = cols.column_name
where
	pks.column_name is null;

-- Prompt user to examine matching data and commit, don't commit, or quit.
-- !x! if(hasrows(ups_stgmatches))
-- !x! andif(hasrows(ups_nk))
	-- !x! if(is_true(!!#display_changes!!))
		-- !x! prompt ask "Do you want to make these changes? For table !!#table!!, new data are shown in the top table below; existing data are in the lower table." sub ~do_updates compare ups_stgmatches and ups_basematches key (!!~pkcollist!!)
	-- !x! endif

	-- !x! if(is_true(!!~do_updates!!))
		-- Create an assignment expression to update non-key columns of the
		-- base table (un-aliased) from columns of the staging table (as s).
		-- !x! if(view_exists(ups_assexpr))
			drop view ups_assexpr cascade;
		-- !x! endif
		create view ups_assexpr as
		select
			group_concat(concat('b.', column_name, ' = s.', column_name) separator ', ') as col
		from
			ups_nk;
		-- !x! subdata ~assexpr ups_assexpr

		-- Create an UPDATE statement to update the base table with
		-- non-key columns from the staging table.  No semicolon terminating generated SQL.
		-- !x! sub ~updatestmt UPDATE !!#table!! as b, !!#stage_pfx!!!!#table!! as s SET !!~assexpr!! WHERE !!~joinexpr!! 
	-- !x! endif
-- !x! endif


-- Create a select statement to find all rows of the staging table
-- that are not in the base table.
-- !x! if(view_exists(ups_newrows))
	drop view ups_newrows cascade;
-- !x! endif
create view ups_newrows as
with newpks as (
	select !!~pkcollist!! from !!#stage_pfx!!!!#table!!
	except
	select !!~pkcollist!! from !!#table!!
	)
select
	s.*
from
	!!#stage_pfx!!!!#table!! as s
	inner join newpks using (!!~pkcollist!!);


-- Prompt user to examine new data and continue or quit.
-- !x! if(hasrows(ups_newrows))
	-- !x! if(is_true(!!#display_changes!!))
		-- !x! prompt ask "Do you want to add these new data to the !!#table!! table?" sub ~do_inserts display ups_newrows
	-- !x! endif

	-- !x! if(is_true(!!~do_inserts!!))
		-- Create an insert statement.  No semicolon terminating generated SQL.
		-- !x! sub ~insertstmt INSERT INTO !!#table!! (!!~allcollist!!) SELECT !!~allcollist!! FROM ups_newrows
	-- !x! endif
-- !x! endif


-- Run the update and insert statements.

-- !x! if(sub_defined(~updatestmt))
-- !x! andif(is_true(!!~do_updates!!))
	-- !x! write "Updating !!#table!!"
	-- !x! if(sub_defined(logfile))
		-- !x! write "" to !!logfile!!
		-- !x! if(sub_defined(log_sql))
		-- !x! andif(is_true(!!log_sql!!))
			-- !x! write "UPDATE statement for !!#table!!:" to !!logfile!!
			-- !x! write [!!~updatestmt!!] to !!logfile!!
		-- !x! endif
		-- !x! if(sub_defined(log_changes))
		-- !x! andif(is_true(!!log_changes!!))
			-- !x! write "Updates:" to !!logfile!!
			-- !x! export ups_stgmatches append to !!logfile!! as txt
		-- !x! endif
		-- !x! write "" to !!logfile!!
	-- !x! endif
	!!~updatestmt!!;
	-- !x! sub !!#updcntvar!! !!$last_rowcount!!
	-- !x! if(sub_defined(logfile))
		-- !x! write "!!$last_rowcount!! rows of !!#table!! updated." to !!logfile!!
	-- !x! endif
	-- !x! write "    !!$last_rowcount!! rows updated."
-- !x! endif


-- !x! if(sub_defined(~insertstmt))
-- !x! andif(is_true(!!~do_inserts!!))
	-- !x! write "Adding data to !!#table!!"
	-- !x! if(sub_defined(logfile))
		-- !x! write "" to !!logfile!!
		-- !x! if(sub_defined(log_sql))
		-- !x! andif(is_true(!!log_sql!!))
			-- !x! write "INSERT statement for !!#table!!:" to !!logfile!!
			-- !x! write [!!~insertstmt!!] to !!logfile!!
		-- !x! endif
		-- !x! if(sub_defined(log_changes))
		-- !x! andif(is_true(!!log_changes!!))
			-- !x! write "New data:" to !!logfile!!
			-- !x! export ups_newrows append to !!logfile!! as txt
		-- !x! endif
		-- !x! write "" to !!logfile!!
	-- !x! endif
	!!~insertstmt!!;
	-- !x! sub !!#inscntvar!! !!$last_rowcount!!
	-- !x! if(sub_defined(logfile))
		-- !x! write "!!$last_rowcount!! rows added to !!#table!!." to !!logfile!!
	-- !x! endif
	-- !x! write "    !!$last_rowcount!! rows added."
-- !x! endif


-- !x! if(is_true(!!#display_final!!))
	-- !x! prompt message "Table !!#table!! after updates and inserts." display !!#table!!
-- !x! endif

-- Clean up.
drop view if exists ups_newrows cascade;
-- !x! if(view_exists(ups_assexpr))
	drop view ups_assexpr cascade;
-- !x! endif
drop view if exists ups_stgmatches cascade;
drop view if exists ups_basematches cascade;
drop view if exists ups_joinexpr cascade;
drop view if exists ups_pkcollist cascade;
drop view if exists ups_allstgcollist cascade;
drop view if exists ups_allbasecollist cascade;
drop view if exists ups_allcollist cascade;
drop view if exists ups_nk cascade;
drop table if exists ups_pks cascade;
drop table if exists ups_cols cascade;


-- !x! END SCRIPT
-- ###################  End of UPSERT_ONE  ########################
-- ################################################################




-- ################################################################
--			Script UPSERT_ALL
--
-- Updates multiple base tables with new or revised data from
-- staging tables, using the UPSERT_ONE script.
--
-- Input parameters:
--		stage_pfx		: The name of the staging schema.
--		control_table	: The name of a table containing at least the
--							following four columns:
--								table_name	: The name of a table
--												  to be updated.
--								exclude_cols	: A comma-delimited
--													list of single-
--													quoted column
--													names, as required
--													by UPDATE_ANY.
--								display_changes	: A value of "Yes" or
--													"No" indicating
--													whether the changes
--													for the table should
--													be displayed.
--								display_final	: A value of "Yes" or
--													"No" indicating
--													whether the final
--													state of the table
--													should be displayed.
--							A table with these columns will be created
--							by the script STAGED_TO_LOAD.
--
-- Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A boolean variable indicating whether
--							the update and insert statements should
--							also be written to the logfile.
--
-- Tables and view used:
--		ups_dependencies			: table
--		ups_ordered_tables			: table
--		ups_proctables				: table
--		ups_toprocess				: view
--		ups_upsert_rows				: view
-- ===============================================================

-- !x! BEGIN SCRIPT UPSERT_ALL with parameters (stage_pfx, control_table)

-- Validate contents of control table
-- !x! execute script validate_control with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!, script=!!$CURRENT_SCRIPT_NAME!!, script_line=!!$SCRIPT_LINE!!)

-- Initialize the status and progress bars if the console is running.
-- !x! if(console_on)
	-- !x! reset counter 221585944
	-- !x! console status "Merging data"
	-- !x! console progress 0
	-- !x! if(view_exists(ups_upsert_rows))
		drop view if exists ups_upsert_rows;
	-- !x! endif
	create view ups_upsert_rows as
	select count(*) + 1 as upsert_rows
	from !!#control_table!!;
	-- !x! subdata upsert_progress_denom ups_upsert_rows
-- !x! endif


-- Get a table of all dependencies for the base schema.
-- !x! if(table_exists(ups_dependencies))
	drop table if exists ups_dependencies cascade;
-- !x! endif
create table ups_dependencies
select distinct
	tc.table_name as child,
	tc_uq.table_name as parent
from
	(select distinct constraint_catalog, constraint_schema, constraint_name,
		table_name,
		unique_constraint_catalog, unique_constraint_schema, unique_constraint_name,
        referenced_table_name
		from information_schema.referential_constraints
		where constraint_schema = '!!$DB_NAME!!') as rc
	inner join (select * from information_schema.table_constraints
			where constraint_type = 'FOREIGN KEY') as tc
		on tc.constraint_catalog = rc.constraint_catalog
		and tc.constraint_schema = rc.constraint_schema
		and tc.constraint_name = rc.constraint_name
        and tc.table_name = rc.table_name
	inner join (select * from information_schema.table_constraints
			where constraint_type not in ('FOREIGN KEY', 'CHECK') ) as tc_uq
		on tc_uq.constraint_catalog = rc.unique_constraint_catalog
		and tc_uq.constraint_schema = rc.unique_constraint_schema
		and tc_uq.constraint_name = rc.unique_constraint_name
		and tc_uq.table_name = rc.referenced_table_name
	;

-- Create a list of tables in the base schema ordered by dependency.
-- !x! if(table_exists(ups_ordered_tables))
	drop table if exists ups_ordered_tables cascade;
-- !x! endif
create table ups_ordered_tables
with recursive dep_depth as (
	select
  		dep.child as first_child,
  		dep.child,
  		dep.parent,
  		1 as lvl
	from
		ups_dependencies as dep
	union all
	select
		dd.first_child,
		dep.child,
		dep.parent,
		dd.lvl + 1 as lvl
	from
		dep_depth as dd
		inner join ups_dependencies as dep on dep.parent = dd.child
			and dep.child <> dd.parent
			and not (dep.parent = dd.first_child and dd.lvl > 2)
 	)
select
	table_name,
	table_order
from (
	-- All parents
	select
		dd.parent as table_name,
		max(lvl) as table_order
	from
		dep_depth as dd
	group by
		table_name
	union
	-- Children that are not parents
	select
		dd.child as table_name,
		max(lvl) + 1 as level
	from
		dep_depth as dd
		left join ups_dependencies as dp on dp.parent = dd.child
	where
		dp.parent is null
	group by
		dd.child
	union
	-- Neither parents nor children
	select distinct
		t.table_name,
		0 as level
	from
		information_schema.tables as t
		left join ups_dependencies as p on t.table_name=p.parent
		left join ups_dependencies as c on t.table_name=c.child
	where
		t.table_schema = '!!$DB_NAME!!'
		and t.table_type = 'BASE TABLE'
		and p.parent is null
		and c.child is null
	) as all_levels;


-- Create a list of the selected tables with ordering information.
-- !x! if(table_exists(ups_proctables))
	drop table if exists ups_proctables cascade;
-- !x! endif
create table ups_proctables
select
	ot.table_order,
	tl.table_name,
	tl.exclude_cols,
	tl.display_changes,
	tl.display_final,
	tl.rows_updated,
	tl.rows_inserted,
	0 as processed
from
	!!#control_table!! as tl
	inner join ups_ordered_tables as ot on ot.table_name = tl.table_name
	;

-- Create a view returning a single unprocessed table, in order.
-- !x! if(view_exists(ups_toprocess))
	drop view if exists ups_toprocess cascade;
-- !x! endif
create view ups_toprocess as
select
	table_name, exclude_cols,
	display_changes, display_final,
	rows_updated, rows_inserted
from ups_proctables
where processed = 0
order by table_order
limit 1;

-- Process all tables in order.
-- !x! execute script upsert_all_innerloop with (stage_pfx=!!#stage_pfx!!)

-- Move the update/insert counts back into the control table.
update !!#control_table!! as ct, ups_proctables as pt
set
	ct.rows_updated = pt.rows_updated,
	ct.rows_inserted = pt.rows_inserted
where
	pt.table_name = ct.table_name;


-- Clean up
drop table if exists ups_proctables cascade;
drop view if exists ups_toprocess cascade;
drop table if exists ups_ordered_tables cascade;
drop table if exists ups_dependencies cascade;
-- !x! if(view_exists(ups_upsert_rows))
	drop view if exists ups_upsert_rows;
-- !x! endif


-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console status "Data merge complete"
	-- !x! console progress 0
-- !x! endif


-- !x! END SCRIPT
--					UPSERT_ALL
-- ****************************************************************
-- ****************************************************************
--		Script UPSERT_ALL_INNERLOOP
-- ---------------------------------------------------------------

-- !x! BEGIN SCRIPT UPSERT_ALL_INNERLOOP with parameters (stage_pfx)

-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console progress !!$counter_221585944!! / !!upsert_progress_denom!!
-- !x! endif

-- !x! if(hasrows(ups_toprocess))
	-- Create variables to store the row counts from updates and inserts.
	-- !x! sub ~rows_updated 0
	-- !x! sub ~rows_inserted 0

	-- !x! select_sub ups_toprocess
	-- !x! execute script upsert_one with (stage_pfx=!!#stage_pfx!!, table=!!@table_name!!, exclude_cols="!!@exclude_cols!!", display_changes=!!@display_changes!!, display_final=!!@display_final!!, updcntvar=+rows_updated, inscntvar=+rows_inserted)

	update ups_proctables
	set rows_updated = !!~rows_updated!!,
		rows_inserted = !!~rows_inserted!!
	where table_name = '!!@table_name!!';

	update ups_proctables
	set processed = 1
	where table_name = '!!@table_name!!';
	-- !x! execute script upsert_all_innerloop with (stage_pfx=!!#stage_pfx!!)
-- !x! endif

-- !x! END SCRIPT
-- ###############  End of UPSERT_ALL_INNERLOOP  ##################
-- ################################################################




-- ################################################################
--			Script QA_ALL
--
-- Conducts null, primary key, and foreign key checks on multiple
-- staging tables containing new or revised data for staging tables,
-- using the NULLQA_ONE, PKQA_ONE, and FKQA_ONE scripts.
--
-- Input parameters:
--		stage_pfx			: The name of the staging schema.
--		control_table	: The name of a table containing at least the
--							following four columns:
--								table_name		: The name of a table
--												  to be updated.
--								null_errors		: For a comma-separated
--													list of columns that
--													are non-nullable in
--													the base table but
--													null in the staging
--													table.
--								pk_errors		: For a count of the number
--													of distinct primary key
--													values that are duplicated,
--													followed by the total row
--													count for the duplicated
--													keys.
--								fk_errors		: For a comma-separated
--													list of foreign-key
--													constraint names that
--													are not met by the
--													staging table.
--							A table with these columns will be created
--							by the script STAGED_TO_LOAD.
--
-- Global variables:
--		logfile			: The name of a log file to which error
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' to indicate whether
--							the SQL that is generated for each foreign
--							key check is written to the logfile.  Optional.
--		log_errors		: A value of 'Yes' or 'No' to indicate whether
--							foreign key errors are written to the logfile.
--							Optional.
--
-- Tables and views used:
--		ups_proctables				: temporary table
--		ups_toprocess				: temporary table
--		ups_upsert_rows				: temporary table
--
-- Counters used:
--		221585944					: Progress bar position
--
-- Global variables modified:
--		upsert_progress_denom		: Created or redefined.
--
-- ===============================================================

-- !x! BEGIN SCRIPT QA_ALL with parameters (stage_pfx, control_table)

-- Initialize the status and progress bars if the console is running.
-- !x! begin script update_console_qa with parameters (check_type, control_table)
-- !x! if(console_on)
	-- !x! reset counter 221585944
	-- !x! console status "Performing !!#check_type!! QA checks"
	-- !x! console progress 0
	-- !x! if(view_exists(ups_upsert_rows))
		drop view if exists ups_upsert_rows;
	-- !x! endif
	create view ups_upsert_rows as
	select count(*) + 1 as upsert_rows
	from !!#control_table!!;
	-- !x! subdata upsert_progress_denom ups_upsert_rows
-- !x! endif
-- !x! end script

-- Create a list of the selected tables with a loop control flag.
-- !x! if(table_exists(ups_proctables))
	drop table if exists ups_proctables cascade;
-- !x! endif
create table ups_proctables
select
	tl.table_name,
	tl.exclude_null_checks,
	tl.display_changes,
	0 as processed
from
	!!#control_table!! as tl
	;

-- Create a view returning a single unprocessed table, in order.
-- !x! if(table_exists(ups_toprocess))
	drop view if exists ups_toprocess;
-- !x! endif
create view ups_toprocess as
select table_name, exclude_null_checks, display_changes
from ups_proctables
where processed = 0
limit 1;

-- Perform null QA checks on all tables.
-- !x! execute script update_console_qa with args (check_type=NULL, control_table=!!#control_table!!)
-- !x! execute script qa_all_nullloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)

-- Perform primary key QA checks on all tables.
update ups_proctables set processed = 0;
-- !x! execute script update_console_qa with args (check_type="primary key", control_table=!!#control_table!!)
-- !x! execute script qa_all_pkloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)

-- Perform foreign key QA checks on all tables.
update ups_proctables set processed = 0;
-- !x! execute script update_console_qa with args (check_type=foreign key, control_table=!!#control_table!!)
-- !x! execute script qa_all_fkloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)


-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console status "Data QA checks complete"
	-- !x! console progress 0
-- !x! endif

-- Clean up.
drop table if exists ups_proctables cascade;
drop view if exists ups_toprocess;
-- !x! if(view_exists(ups_upsert_rows))
	drop view if exists ups_upsert_rows;
-- !x! endif


-- !x! END SCRIPT
--					QA_ALL
-- ****************************************************************
-- ****************************************************************
--		Script QA_ALL_NULLLOOP
-- ---------------------------------------------------------------

-- !x! BEGIN SCRIPT QA_ALL_NULLLOOP with parameters (stage_pfx, control_table)

-- !x! sub_empty ~ups_null_error_list

-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console progress !!$counter_221585944!! / !!upsert_progress_denom!!
-- !x! endif

-- !x! if(hasrows(ups_toprocess))
	-- !x! select_sub ups_toprocess
	-- !x! if(is_null("!!@exclude_null_checks!!"))
		-- !x! execute script nullqa_one with (stage_pfx=!!#stage_pfx!!, table=!!@table_name!!, error_list=+ups_null_error_list)
	-- !x! else
		-- !x! execute script nullqa_one with (stage_pfx=!!#stage_pfx!!, table=!!@table_name!!, error_list=+ups_null_error_list, exclude_null_checks=[!!@exclude_null_checks!!])
	-- !x! endif
	-- !x! if(not is_null("!!~ups_null_error_list!!"))
		update !!#control_table!!
		set null_errors = '!!~ups_null_error_list!!'
		where table_name = '!!@table_name!!';
	-- !x! endif

	update ups_proctables
	set processed = 1
	where table_name = '!!@table_name!!';
	-- !x! execute script qa_all_nullloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)
-- !x! endif

-- !x! END SCRIPT
--					QA_ALL_NULLLOOP
-- ****************************************************************
-- ****************************************************************
--		Script QA_ALL_PKLOOP
-- ---------------------------------------------------------------

-- !x! BEGIN SCRIPT QA_ALL_PKLOOP with parameters (stage_pfx, control_table)

-- !x! sub_empty ~ups_pk_error_list
-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console progress !!$counter_221585944!! / !!upsert_progress_denom!!
-- !x! endif

-- !x! if(hasrows(ups_toprocess))
	-- !x! select_sub ups_toprocess
	-- !x! execute script pkqa_one with (stage_pfx=!!#stage_pfx!!, table=!!@table_name!!, display_errors=!!@display_changes!!, error_list=+ups_pk_error_list)
	-- !x! if(not is_null("!!~ups_pk_error_list!!"))
		update !!#control_table!!
		set pk_errors = '!!~ups_pk_error_list!!'
		where table_name = '!!@table_name!!';
	-- !x! endif
	
	update ups_proctables
	set processed = 1
	where table_name = '!!@table_name!!';
	-- !x! execute script qa_all_pkloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)
-- !x! endif


-- !x! END SCRIPT
--					QA_ALL_PKLOOP
-- ****************************************************************
-- ****************************************************************
--		Script QA_ALL_FKLOOP
-- ---------------------------------------------------------------

-- !x! BEGIN SCRIPT QA_ALL_FKLOOP with parameters (stage_pfx, control_table)

-- !x! sub_empty ~ups_error_list

-- Update the status bar if the console is running.
-- !x! if(console_on)
	-- !x! console progress !!$counter_221585944!! / !!upsert_progress_denom!!
-- !x! endif

-- !x! if(hasrows(ups_toprocess))
	-- !x! select_sub ups_toprocess
	-- !x! execute script fkqa_one with (stage_pfx=!!#stage_pfx!!, table=!!@table_name!!, display_errors=!!@display_changes!!, error_list=+ups_error_list)
	-- !x! if(not is_null("!!~ups_error_list!!"))
		update !!#control_table!!
		set fk_errors = '!!~ups_error_list!!'
		where table_name = '!!@table_name!!';
	-- !x! endif

	update ups_proctables
	set processed = 1
	where table_name = '!!@table_name!!';
	-- !x! execute script qa_all_fkloop with (stage_pfx=!!#stage_pfx!!, control_table=!!#control_table!!)
-- !x! endif

-- !x! END SCRIPT
-- #####################  End of QA_ALL  ###########################
-- #################################################################




-- ################################################################
--			Script UPDTPK_ONE
--
-- Updates primary keys in the base table, based on new and existing
-- values of PK columns in a staging table, using UPDATE
-- statements.  Displays data to be modified to the
-- user before any modifications are done.  Reports the changes
-- made to the console and optionally to a log file.
--
-- Input parameters:
--		stage_pfx		: The prefix to the name of the staging table.
--		table			: The table name--same for base and staging,
--							except for the prefix on the staging table.
--		display_errors	: A value of 'Yes' or 'No' to indicate whether
--							any errors should be displayed in a GUI.
--		display_changes	: A value of 'Yes' or 'No' to indicate whether
--							or not the changes to be made to the 
--							base table should be displayed in a GUI.
--
--	Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' indicating whether
--							the update and insert statements should
--							also be written to the logfile.  Optional.
--		log_changes		: A value of 'Yes' or 'No' indicating whether
--							the updated and inserted data should be
--							written to the logfile.  Optional.
--
-- Tables and views created or modified:
--		ups_pkqa_errors			: temporary table
--		ups_pkcol_info			: temporary table
--		ups_pkupdates			: temporary table
--		ups_pkupdate_strings	: temporary view
--		ups_pkupdates			: temporary table
-- ===============================================================

-- !x! BEGIN SCRIPT UPDTPK_ONE with parameters (stage_pfx, table, display_errors, display_changes)

-- !x! if(console_on)
	-- !x! console status "Primary key updates"
-- !x! endif


-- Validate inputs: base/staging schemas and table
-- !x! execute script validate_one with args (stage_pfx=!!#stage_pfx!!, table=!!#table!!, script=!!$CURRENT_SCRIPT!!, script_line=!!$SCRIPT_LINE!!)

-- Write an initial header to the logfile.
-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- Performing primary key updates on table !!#table!! from !!#stage_pfx!!!!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Performing primary key updates on table !!#table!! from !!#stage_pfx!!!!#table!!"

-- Create a temp table to store the results of the PK update QA checks
-- !x! if(table_exists(ups_pkqa_errors))
	drop table if exists ups_pkqa_errors cascade; 
-- !x! endif
create table ups_pkqa_errors (
	error_code varchar(40),
	error_description varchar(500)
);


-- Populate a (temporary) table with the names of the primary key columns of the base table.
-- Get the old and new primary key columns from staging table into various formats
-- to use later to construct SQL statement to select records in various ways for both updates and QA checks.
-- Include column lists, join expression, and where clause
-- !x! if(table_exists(ups_pkcol_info))
	drop table if exists ups_pkcol_info cascade; 
-- !x! endif
create table ups_pkcol_info
select 
	k.table_schema,
	k.table_name,
	k.column_name,
	cast(concat('b.', column_name) as varchar(2000)) as base_aliased,
	cast(concat('s.', column_name) as varchar(2000)) as staging_aliased,
	cast(concat('s.', column_name, ' as staging_', column_name) as varchar(2000)) as staging_aliased_prefix,
	cast(concat('b.', column_name, ' = s.', column_name) as varchar(2000)) as join_expr,
	cast(concat('new_', column_name) as varchar(2000)) as newpk_col,
	cast(concat('s.new_', column_name) as varchar(2000)) as newpk_col_aliased,
	cast(concat('new_', column_name, ' is null') as varchar(2000)) as newpk_col_empty,
	cast(concat('new_', column_name, ' is not null') as varchar(2000)) as newpk_col_not_empty,
	cast(concat('b.', column_name, ' = s.new_', column_name) as varchar(2000)) as assmt_expr,
	cast(concat('b.', column_name, ' = s.new_', column_name) as varchar(2000)) as join_expr_oldnew, 
	cast(concat('s.new_', column_name, ' = b.new_', column_name) as varchar(2000)) as join_expr_new,
	k.ordinal_position
from information_schema.table_constraints as tc
inner join information_schema.key_column_usage as k
	on tc.constraint_type = 'PRIMARY KEY' 
	and tc.constraint_name = k.constraint_name
	and tc.constraint_catalog = k.constraint_catalog
	and tc.constraint_schema = k.constraint_schema
	and tc.table_schema = k.table_schema
	and tc.table_name = k.table_name
	and tc.constraint_name = k.constraint_name
where
	k.table_name = '!!#table!!'
	and k.table_schema = '!!$DB_NAME!!'
;


-- Run QA checks
-- !x! execute script UPDTPKQA_ONE with arguments(stage_pfx=!!#stage_pfx!!, table=!!#table!!, pkinfo_table=ups_pkcol_info, qaerror_table=ups_pkqa_errors, display_errors=!!#display_errors!!)


-- Run the PK update ONLY if QA check script returned no errors
-- !x! if(not hasrows(ups_pkqa_errors))
	-- !x! rm_sub ~updatestmt
	
	-- !x! sub ~do_updates Yes

	-- !x! if(sub_defined(logfile))
		-- !x! write "" to !!logfile!!
		-- !x! write "==================================================================" to !!logfile!!
		-- !x! write "!!$current_time!! -- Performing primary key update on table !!#table!!" to !!logfile!!
	-- !x! endif

	-- !x! if(console_on)
		-- !x! console status "Performing PK updates"
		-- !x! console progress 0
	-- !x! endif
	
	-- !x! write "Performing primary key update on table !!#table!!"
	
	-- Create strings necessary to construct SQL to perform the updates
	-- !x! if(view_exists(ups_pkupdate_strings))
		drop view if exists ups_pkupdate_strings cascade;
	-- !x! endif
	create view ups_pkupdate_strings as
	select 
		group_concat(base_aliased order by ordinal_position separator ', ') as oldpk_cols,
		group_concat(newpk_col order by ordinal_position separator ', ') as newpk_cols,
		group_concat(join_expr order by ordinal_position separator ' and ') as joinexpr,
		group_concat(newpk_col_not_empty order by ordinal_position separator ' and ') as all_newpk_col_not_empty,
		group_concat(assmt_expr order by ordinal_position separator ', ') as assmt_expr
	from ups_pkcol_info
	group by table_name
	;
	-- !x! select_sub ups_pkupdate_strings
	
	-- Create a FROM clause for an inner join between base and staging
	-- tables on the primary key column(s).
	-- !x! sub ~fromclause FROM !!#table!! as b INNER JOIN !!#stage_pfx!!!!#table!! as s ON !!@joinexpr!!
	
	-- Create a WHERE clause for the rows to include in the selection (only those having new PK columns populated in the staging table)
	-- !x! sub ~whereclause WHERE !!@all_newpk_col_not_empty!!
	
	-- Select all matches for PK update into temp table
	-- !x! if(table_exists(ups_pkupdates))
		drop table if exists ups_pkupdates cascade; 
	-- !x! endif
	create table ups_pkupdates
	select 
		!!@oldpk_cols!!,
		!!@newpk_cols!!
	!!~fromclause!!
	!!~whereclause!!
	;

	-- Prompt user to examine matching data and commit, don't commit, or quit.
	-- !x! if(hasrows(ups_pkupdates))
		-- !x! if(is_true(!!#display_changes!!))
			-- !x! prompt ask "Do you want to make these changes to primary key values for table !!#table!!?" sub ~do_updates display ups_pkupdates
		-- !x! endif
		-- !x! if(is_true(!!~do_updates!!))
			
			-- Create an UPDATE statement to update PK columns of the base table with
			-- "new" PK columns from the staging table.  No semicolon terminating generated SQL.
			-- !x! sub ~updatestmt UPDATE !!#table!! as b, !!#stage_pfx!!!!#table!! as s SET !!@assmt_expr!! WHERE !!@joinexpr!! and !!@all_newpk_col_not_empty!!
			
			-- !x! write "Updating !!#table!!"
			-- !x! if(sub_defined(logfile))
				-- !x! write "" to !!logfile!!
				-- !x! if(sub_defined(log_sql))
				-- !x! andif(is_true(!!log_sql!!))
					-- !x! write "UPDATE statement for !!#table!!:" to !!logfile!!
					-- !x! write [!!~updatestmt!!] to !!logfile!!
				-- !x! endif
				-- !x! if(sub_defined(log_changes))
				-- !x! andif(is_true(!!log_changes!!))
					-- !x! write "Updates:" to !!logfile!!
					-- !x! export ups_pkupdates append to !!logfile!! as txt
				-- !x! endif
				-- !x! write "" to !!logfile!!
			-- !x! endif
			!!~updatestmt!!;
			-- !x! if(sub_defined(logfile))
				-- !x! write "!!$last_rowcount!! rows of !!#table!! updated." to !!logfile!!
			-- !x! endif
			-- !x! write "    !!$last_rowcount!! rows updated."		
		-- !x! endif
	-- !x! else
		--!x! write "No primary key updates specified for existing records in !!#table!!"	
	-- !x! endif
-- !x! endif


-- !x! if(table_exists(ups_pkqa_errors))
	drop table if exists ups_pkqa_errors cascade; 
-- !x! endif
-- !x! if(table_exists(ups_pkcol_info))
	drop table if exists ups_pkcol_info cascade; 
-- !x! endif
-- !x! if(view_exists(ups_pkupdate_strings))
	drop view if exists ups_pkupdate_strings cascade;
-- !x! endif
-- !x! if(table_exists(ups_pkupdates))
	drop table if exists ups_pkupdates cascade; 
-- !x! endif


-- !x! END SCRIPT
-- ###################  End of UPDTPK_ONE  ########################
-- ################################################################


-- ################################################################
--			Script UPDTPKQA_ONE
--
-- Performs QA checks on requested primary key updates to a table,
-- based on old and new values of the table's primary key columns
-- in a staging table.
--
-- Input parameters:
--		stage_pfx		: The prefix to the staging table name.
--		table			: The table name--same for base and staging,
--							except for the prefix of the staging table.
--		pkinfo_table	: The name of a temporary table to be passed by
--							the caller that contains information about the table PK,
--							including strings to be used in constructing
--							SQL for checks
--		qaerror_table  	: The name of a temporary table to
--							store any errors found by QA checks.
--		display_errors	: A value of 'Yes' or 'No' to indicate whether
--							any errors should be displayed in a GUI.
--	Output parameters:
--		error_list		: The name of the variable to receive FILL IN.
--
--	Global variables:
--		logfile			: The name of a log file to which update
--							messages will be written.  Optional.
--		log_sql			: A value of 'Yes' or 'No' indicating whether
--							the update and insert statements should
--							also be written to the logfile.  Optional.
--							Currently only writes SQL for foreign key checks
--							(final check) to log.
--		log_errors		: A value of 'Yes' or 'No' to indicate whether
--							errors are written to the logfile. Optional.
--
-- Tables and views created or modified:
--		ups_missing_pk_cols			: temporary table
--		ups_pkqa_str_lib			: tempoarary table
--		ups_any_pk_cols				: temporary table
--		ups_empty_pk_cols			: temporary table
--		ups_empty_pk_cols_rwcnt		: temporary view
--		ups_old_pks_wc				: temporary table
--		ups_invalid_old_pks			: temporary table
--		ups_invld_pk_rwcnt			: temporary view
--		ups_existing_new_pks		: temporary table 
--		ups_exst_nwpk_rwcnt			: temporary view
--		ups_pk_mapping_conflict		: temporary table
--		ups_map_conf_rwcnt			: temporary view
--		ups_pk_duplicate_keys		: temporary table
--		ups_dup_key_rwcnt			: temporary view
--		ups_fkcol_refs				: temporary table
--		ups_pkcol_deps				: temporary table
--		ups_pkfk_ctrl				: temporary table
-- ===============================================================

-- !x! BEGIN SCRIPT UPDTPKQA_ONE with parameters (stage_pfx, table, pkinfo_table, qaerror_table, display_errors)

-- Write an initial header to the logfile.
-- !x! if(sub_defined(logfile))
	-- !x! write "" to !!logfile!!
	-- !x! write "==================================================================" to !!logfile!!
	-- !x! write "!!$current_time!! -- QA checks for primary key updates on table !!#table!!" to !!logfile!!
-- !x! endif

-- !x! write "Conducting QA checks on table !!#stage_pfx!!!!#table!! for primary key updates to table !!#table!!"

-- Initialize the status and progress bars if the console is running.
-- !x! if(console_on)
	-- !x! console status "QA checks for PK updates on !!#table!!"
-- !x! endif


-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Check 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- No primary key constraint on base table
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- !x! if(not hasrows(!!#pkinfo_table!!))

	-- !x! sub ~error_description No primary key constraint on base table !!#table!!
	-- !x! write "    !!~error_description!!"
	-- !x! if(sub_defined(logfile))
		-- !x! write "" to !!logfile!!
		-- !x! write "!!~error_description!!" to !!logfile!!
	-- !x! endif
	insert into !!#qaerror_table!! (error_code, error_description)
	values ('No PK on base table', '!!~error_description!!')
	;

-- No other QA checks are conducted if this check fails:
-- Remaining QA checks are conducted ONLY if base table has PK
-- !x! else
	
	-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	-- Check 2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	-- A "new" PK column exists in staging table for every PK column of base table
	-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
	-- Find any MISSING PK columns in staging table
	-- !x! if(table_exists(ups_missing_pk_cols))
		drop table if exists ups_missing_pk_cols cascade; 
	-- !x! endif
	create table ups_missing_pk_cols
	select 
		group_concat(newpk_col order by ordinal_position separator ', ') as missing_newpk_cols
	from
		--Base table PK columns, with expected name in staging table ("new_" prepended to column name)
		!!#pkinfo_table!! as pk
		--Staging table columns
		left join 
			(
			select table_name, column_name 
			from information_schema.columns
			where
				table_schema = '!!$DB_NAME!!'
				and table_name = '!!#stage_pfx!!!!#table!!'
			) as stag on  pk.newpk_col=stag.column_name
	where
		stag.column_name is null
	having count(*)>0
	; 
	
	-- !x! if(hasrows(ups_missing_pk_cols))
		
		-- !x! subdata ~error_info ups_missing_pk_cols
		
		-- !x! sub ~error_description New primary key column(s) missing from staging table: !!~error_info!!
		
		-- !x! write "    !!~error_description!!"
		-- !x! if(sub_defined(logfile))
			-- !x! write "" to !!logfile!!
			-- !x! write "!!~error_description!!" to !!logfile!!
		-- !x! endif
		insert into !!#qaerror_table!! (error_code, error_description)
		values ('Missing new PK column(s)', '!!~error_description!!')
		;
	
	-- No other QA checks are conducted if this check fails:
	-- Remaining QA checks are all conducted ONLY if all expected "new PK" columns exist in staging table	
	-- !x! else 
	
		-- Library of aggregated strings used to construct SQL for the remaining checks
		
		-- Just base table
		-- !x! sub ~base_table !!#table!!
		
		-- Just staging table
		-- !x! sub ~staging_table !!#stage_pfx!!!!#table!!
		
		-- !x! if(table_exists(ups_pkqa_str_lib))
			drop table if exists ups_pkqa_str_lib;
		-- !x! endif
		create table ups_pkqa_str_lib
		select
			group_concat(column_name order by ordinal_position separator ', ') as old_pkcol,
			group_concat(staging_aliased order by ordinal_position separator ', ') as old_pkcol_aliased,
			group_concat(staging_aliased_prefix order by ordinal_position separator ', ') as old_pkcol_aliased_prefix,
			group_concat(newpk_col order by ordinal_position separator ', ') as new_pkcol,
			group_concat(newpk_col_aliased order by ordinal_position separator ', ') as new_pkcol_aliased,
			group_concat(join_expr order by ordinal_position separator ' and ') as joincond_origorig,
			group_concat(join_expr_oldnew order by ordinal_position separator ' and ') as joincond_oldnew,
			group_concat(join_expr_new order by ordinal_position separator ' and ') as joincond_newnew,
			group_concat(newpk_col_not_empty order by ordinal_position separator ' or ') as any_newpk_col_not_empty,
			group_concat(newpk_col_not_empty order by ordinal_position separator ' and ') as all_newpk_col_not_empty,
			group_concat(newpk_col_empty order by ordinal_position separator ' or ') as any_newpk_col_empty,
			group_concat(newpk_col_empty order by ordinal_position separator ' and ') as all_newpk_col_empty
		from !!#pkinfo_table!!
		;
		-- !x! select_sub ups_pkqa_str_lib
		
		
		
		-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		-- Check 3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		-- There are any rows with PK updates specified.
		-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
		-- Find any populated new PK columns in staging table
		-- !x! if(table_exists(ups_any_pk_cols))
			drop table if exists ups_any_pk_cols cascade; 
		-- !x! endif
		create table ups_any_pk_cols
		select *
		from !!~staging_table!! 
		where !!@any_newpk_col_not_empty!!
		;
		-- !x! if(not hasrows(ups_any_pk_cols))
			-- !x! sub ~error_description No primary key updates specified in !!#stage_pfx!!!!#table!!
			-- !x! write "    !!~error_description!!"
			-- !x! if(sub_defined(logfile))
				-- !x! write "" to !!logfile!!
				-- !x! write "!!~error_description!!" to !!logfile!!
			-- !x! endif
			insert into !!#qaerror_table!! (error_code, error_description)
			values ('No PK updates specified in staging table', '!!~error_description!!')
			;
		-- No other QA checks are conducted if this check fails
		-- !x! else
		
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Where any "new" PK column is populated in the staging table, they are all populated.
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		
			-- Construct SQL statement looking for any NULLs in "new" PK columns in rows where any PK columns are populated
			-- Find any EMPTY PK columns in staging table
			-- !x! if(table_exists(ups_empty_pk_cols))
				drop table if exists ups_empty_pk_cols cascade; 
			-- !x! endif
			create table ups_empty_pk_cols
			select
				!!@old_pkcol!!,
				!!@new_pkcol!!
			from	
				!!~staging_table!! 
			where
				not (!!@all_newpk_col_empty!!)
				and (!!@any_newpk_col_empty!!)
			;

			-- !x! if(hasrows(ups_empty_pk_cols))
				-- !x! if(view_exists(ups_empty_pk_cols_rwcnt))
					drop view if exists ups_empty_pk_cols_rwcnt cascade;
				-- !x! endif
				create view ups_empty_pk_cols_rwcnt as
				select count(*) as rwcnt
				from ups_empty_pk_cols
				;
				-- !x! subdata ~rowcount ups_empty_pk_cols_rwcnt
				-- !x! sub ~error_description Missing values in new PK columns in !!#stage_pfx!!!!#table!!: !!~rowcount!! row(s)
				-- !x! write "    !!~error_description!!"
				insert into !!#qaerror_table!! (error_code, error_description)
				values ('Incomplete mapping', '!!~error_description!!')
				;	
				-- !x! if(sub_defined(logfile))
					-- !x! write "" to !!logfile!!
					-- !x! write "!!~error_description!!" to !!logfile!!
					-- !x! if(sub_defined(log_errors))
					-- !x! andif(is_true(!!log_errors!!))
						-- !x! export ups_empty_pk_cols append to !!logfile!! as txt
					-- !x! endif
				-- !x! endif
				-- !x! if(is_true(!!#display_errors!!))
					-- !x! prompt message "Missing values in new PK columns in !!#stage_pfx!!!!#table!!" display ups_empty_pk_cols
				-- !x! endif	
			-- !x! endif
			
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 5 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Where any "new" PK column is populated in the staging table, the value of the original PK for that row is valid
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			
			-- New PK col in staging table are not empty
			-- !x! if(table_exists(ups_old_pks_wc))
				drop table if exists ups_old_pks_wc cascade; 
			-- !x! endif
			create table ups_old_pks_wc
			select base_aliased
			from !!#pkinfo_table!!
			order by ordinal_position
			limit 1;
			-- !x! subdata ~old_pk_firstcol ups_old_pks_wc	
			
			-- !x! if(table_exists(ups_invalid_old_pks))
				drop table if exists ups_invalid_old_pks cascade;
			-- !x! endif
			create table ups_invalid_old_pks
			select
				!!@old_pkcol_aliased!!,
				!!@new_pkcol!!
			from !!~staging_table!! as s
					left join !!~base_table!! as b on !!@joincond_origorig!!
			where !!@all_newpk_col_not_empty!! and !!~old_pk_firstcol!! is null
			;
			
			-- !x! if(hasrows(ups_invalid_old_pks))
				-- !x! if(view_exists(ups_invalid_pk_rwcnt))
					drop view if exists ups_invld_pk_rwcnt cascade;
				-- !x! endif
				create view ups_invld_pk_rwcnt as
				select count(*) as rwcnt
				from ups_invalid_old_pks
				;
				-- !x! subdata ~rowcount ups_invld_pk_rwcnt
				-- !x! sub ~error_description Invalid original PK in !!#stage_pfx!!!!#table!!: !!~rowcount!! row(s)
				-- !x! write "    !!~error_description!!"
				insert into !!#qaerror_table!! (error_code, error_description)
				values ('Invalid old PK value', '!!~error_description!!')
				;	
				-- !x! if(sub_defined(logfile))
					-- !x! write "" to !!logfile!!
					-- !x! write "!!~error_description!!" to !!logfile!!
					-- !x! if(sub_defined(log_errors))
					-- !x! andif(is_true(!!log_errors!!))
						-- !x! export ups_invalid_old_pks append to !!logfile!! as txt
					-- !x! endif
				-- !x! endif
				-- !x! if(is_true(!!#display_errors!!))
					-- !x! prompt message "Invalid original PK in !!#stage_pfx!!!!#table!!" display ups_invalid_old_pks
				-- !x! endif		
			-- !x! endif


			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- None of the "new" PK values already exist in the base table
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

			-- !x! if(table_exists(ups_existing_new_pks))
				drop table if exists ups_existing_new_pks cascade;
			-- !x! endif
			create table ups_existing_new_pks
			select 
				!!@old_pkcol_aliased_prefix!!,
				!!@new_pkcol!!,
				b.*
			from !!~staging_table!! as s
					inner join !!~base_table!! as b on !!@joincond_oldnew!!
			;
				
			-- !x! if(hasrows(ups_existing_new_pks))
				-- !x! if(view_exists(ups_exst_nwpk_rwcnt))
					drop view if exists ups_exst_nwpk_rwcnt cascade;
				-- !x! endif
				create view ups_exst_nwpk_rwcnt as
				select count(*) as rwcnt
				from ups_existing_new_pks
				;
				-- !x! subdata ~rowcount ups_exst_nwpk_rwcnt
				-- !x! sub ~error_description New PK already exists in !!#table!!: !!~rowcount!! row(s)
				-- !x! write "    !!~error_description!!"
				insert into !!#qaerror_table!! (error_code, error_description)
				values ('Existing new PK value', '!!~error_description!!')
				;	
				-- !x! if(sub_defined(logfile))
					-- !x! write "" to !!logfile!!
					-- !x! write "!!~error_description!!" to !!logfile!!
					-- !x! if(sub_defined(log_errors))
					-- !x! andif(is_true(!!log_errors!!))
						-- !x! export ups_existing_new_pks append to !!logfile!! as txt
					-- !x! endif
				-- !x! endif
				-- !x! if(is_true(!!#display_errors!!))
					-- !x! prompt message "New PK already exists in !!#table!!" display ups_existing_new_pks
				-- !x! endif		
			-- !x! endif


			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- No two (or more) original PK values map to same new PK value
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			
			-- !x! if(table_exists(ups_pk_mapping_conflict))
				drop table if exists ups_pk_mapping_conflict cascade;
			-- !x! endif
			create table ups_pk_mapping_conflict
			select
				!!@old_pkcol_aliased!!,
				!!@new_pkcol_aliased!!
			from !!~staging_table!! as s
				inner join 
				(
				select 
					!!@new_pkcol!!
				from 
				(select distinct !!@old_pkcol!!, !!@new_pkcol!! from !!~staging_table!! where !!@all_newpk_col_not_empty!!) as a
				group by 
					!!@new_pkcol!!
				having count(*) >1
				) as b on !!@joincond_newnew!!
			;
			
			-- !x! if(hasrows(ups_pk_mapping_conflict))
				-- !x! if(view_exists(ups_map_conf_rwcnt))
					drop view if exists ups_map_conf_rwcnt cascade;
				-- !x! endif
				create view ups_map_conf_rwcnt as
				select count(*) as rwcnt
				from ups_pk_mapping_conflict
				;
				-- !x! subdata ~rowcount ups_map_conf_rwcnt
				-- !x! sub ~error_description Multiple original PKs mapped to same new PK in !!#stage_pfx!!!!#table!!: !!~rowcount!! row(s)
				-- !x! write "    !!~error_description!!"
				insert into !!#qaerror_table!! (error_code, error_description)
				values ('Mapping conflict', '!!~error_description!!')
				;	
				-- !x! if(sub_defined(logfile))
					-- !x! write "" to !!logfile!!
					-- !x! write "!!~error_description!!" to !!logfile!!
					-- !x! if(sub_defined(log_errors))
					-- !x! andif(is_true(!!log_errors!!))
						-- !x! export ups_pk_mapping_conflict append to !!logfile!! as txt
					-- !x! endif
				-- !x! endif
				-- !x! if(is_true(!!#display_errors!!))
					-- !x! prompt message "Multiple original PKs mapped to same new PK in !!#stage_pfx!!!!#table!!" display ups_pk_mapping_conflict
				-- !x! endif		
			-- !x! endif


			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 8 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- No single original PK value maps to multiple new PK values
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			
			-- !x! if(table_exists(ups_pk_duplicate_keys))
				drop table if exists ups_pk_duplicate_keys cascade;
			-- !x! endif
			create table ups_pk_duplicate_keys
			select
				!!@old_pkcol_aliased!!,
				!!@new_pkcol_aliased!!
			from !!~staging_table!! as s
				inner join
				(
				select
					!!@old_pkcol!!
				from
				(select distinct !!@old_pkcol!!, !!@new_pkcol!! from !!~staging_table!! where !!@all_newpk_col_not_empty!!) as a
				group by 
					!!@old_pkcol!!
				having count(*)>1
				) as b on !!@joincond_origorig!!
			;
			
			-- !x! if(hasrows(ups_pk_duplicate_keys))
				-- !x! if(view_exists(ups_dup_key_rwcnt))
					drop view if exists ups_dup_key_rwcnt cascade;
				-- !x! endif
				create view ups_dup_key_rwcnt as
				select count(*) as rwcnt
				from ups_pk_duplicate_keys
				;
				-- !x! subdata ~rowcount ups_dup_key_rwcnt
				-- !x! sub ~error_description Original PK mapped to multiple new PKs in !!#stage_pfx!!!!#table!!: !!~rowcount!! row(s)
				-- !x! write "    !!~error_description!!"
				insert into !!#qaerror_table!! (error_code, error_description)
				values ('Duplicate keys', '!!~error_description!!')
				;	
				-- !x! if(sub_defined(logfile))
					-- !x! write "" to !!logfile!!
					-- !x! write "!!~error_description!!" to !!logfile!!
					-- !x! if(sub_defined(log_errors))
					-- !x! andif(is_true(!!log_errors!!))
						-- !x! export ups_pk_duplicate_keys append to !!logfile!! as txt
					-- !x! endif
				-- !x! endif
				-- !x! if(is_true(!!#display_errors!!))
					-- !x! prompt message "Original PK mapped to multiple new PKs in !!#stage_pfx!!!!#table!!" display ups_pk_duplicate_keys
				-- !x! endif		
			-- !x! endif


			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- Check 9 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			-- If any of the PK columns reference a parent table, all the "new" values of that column are valid
			-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			
			-- Get ALL foreign key column references for the base table
			-- !x! if(table_exists(ups_fkcol_refs))
				drop table if exists ups_fkcol_refs cascade;
			-- !x! endif
			create table ups_fkcol_refs
			select
				rc.constraint_name as fk_constraint,
				cu.table_schema,
				cu.table_name,
				cu.column_name,
				cu.ordinal_position,
				cu_uq.table_schema as parent_schema,
				cu_uq.table_name as parent_table,
				cu_uq.column_name as parent_column,
				cu_uq.ordinal_position as parent_position
			from
				(select constraint_catalog, constraint_schema, constraint_name,
					table_name,
					unique_constraint_catalog, unique_constraint_schema, unique_constraint_name,
            		referenced_table_name
					from information_schema.referential_constraints
					where constraint_schema = '!!$db_name!!'
					) as rc
				inner join (select * from information_schema.table_constraints
					where constraint_type = 'FOREIGN KEY' and constraint_schema = '!!$db_name!!'
					) as tc
					on tc.constraint_catalog = rc.constraint_catalog
					and tc.constraint_schema = rc.constraint_schema
					and tc.constraint_name = rc.constraint_name
            		and tc.table_name = rc.table_name
				inner join (select * from information_schema.table_constraints
					where constraint_type not in ('FOREIGN KEY', 'CHECK')
					and constraint_schema = '!!$db_name!!'
					) as tc_uq
					on tc_uq.constraint_catalog = rc.unique_constraint_catalog
					and tc_uq.constraint_schema = rc.unique_constraint_schema
					and tc_uq.constraint_name = rc.unique_constraint_name
            		and tc_uq.table_name = rc.referenced_table_name
				inner join information_schema.key_column_usage as cu
					on cu.constraint_catalog = tc.constraint_catalog
					and cu.constraint_schema = tc.constraint_schema
					and cu.constraint_name = tc.constraint_name
            		and cu.table_schema = tc.table_schema
					and cu.table_name = tc.table_name
				inner join information_schema.key_column_usage as cu_uq
					on cu_uq.constraint_catalog = tc_uq.constraint_catalog
					and cu_uq.constraint_schema = tc_uq.constraint_schema
					and cu_uq.constraint_name = tc_uq.constraint_name
					and cu_uq.table_schema = tc_uq.table_schema
            		and cu_uq.table_name = tc_uq.table_name
					and cu_uq.ordinal_position = cu.ordinal_position
			where
				rc.table_name = '!!#table!!'
				;

			-- Narrow the list down to ONLY dependencies that affect PK columns
			-- Include not just the PK columns themselves, but ALL columns included in FKs
			-- that include ANY PK columns (probably rare/unlikely that a non-PK column would be
			-- part of the same foreign key as a PK column, but this ensures that ALL columns of the FK 
			-- are included, whether or not the column is part of the PK)
			-- !x! if(table_exists(ups_pkcol_deps))
				drop table if exists ups_pkcol_deps cascade;
			-- !x! endif
			create table ups_pkcol_deps
			select
				refs.*
			from
				ups_fkcol_refs as refs
				inner join
				--Distinct list of FK constraints on the table that include ANY PK columns
					(
					select distinct
						fk_constraint, r.table_schema, r.table_name
					from	
						ups_fkcol_refs as r
						inner join ups_pkcol_info as p on r.table_schema=p.table_schema and r.table_name=p.table_name and r.column_name=p.column_name
					) as const on refs.fk_constraint=const.fk_constraint and refs.table_schema=const.table_schema and refs.table_name=const.table_name
					;
			
			-- Create a control table for looping to check each fk
			-- Include (for later use) some of the constructed strings that apply to the entire PK (not
			-- just the FK being checked)
			-- !x! if(table_exists(ups_pkfk_ctrl))
				drop table if exists ups_pkfk_ctrl cascade;
			-- !x! endif
			create table ups_pkfk_ctrl
			select 
				fk_constraint,
				table_name, parent_table,
				min(parent_column) as any_referenced_column,
				'!!@old_pkcol_aliased!!' as old_pkcol_aliased,
				'!!@new_pkcol!!' as new_pkcol,
				'!!@all_newpk_col_not_empty!!' as all_newpk_col_not_empty,
				False as processed
			from ups_pkcol_deps
			group by	
				fk_constraint, table_name, parent_table
			;
				
			-- Create a view to select one constraint to process.
			-- !x! if(view_exists(ups_next_fk))
				drop view if exists ups_next_fk cascade; 
			-- !x! endif
			create view ups_next_fk as
			select *
			from ups_pkfk_ctrl
			where not processed
			limit 1
			;
			
			--Process all constraints: check every foreign key
			--!x! execute script updtpkqa_one_innerloop with (stage_pfx=!!#stage_pfx!!, qaerror_table=!!#qaerror_table!!, display_errors=!!#display_errors!!)
		-- !x! endif
	-- !x! endif
-- !x! endif

-- !x! if(table_exists(ups_missing_pk_cols))
	drop table if exists ups_missing_pk_cols cascade; 
-- !x! endif
-- !x! if(table_exists(ups_pkqa_str_lib))
	drop table if exists ups_pkqa_str_lib;
-- !x! endif
-- !x! if(table_exists(ups_any_pk_cols))
	drop table if exists ups_any_pk_cols cascade; 
-- !x! endif
-- !x! if(table_exists(ups_empty_pk_cols))
	drop table if exists ups_empty_pk_cols cascade; 
-- !x! endif
-- !x! if(view_exists(ups_empty_pk_cols_rwcnt))
	drop view if exists ups_empty_pk_cols_rwcnt cascade;
-- !x! endif
-- !x! if(table_exists(ups_old_pks_wc))
	drop table if exists ups_old_pks_wc cascade; 
-- !x! endif
-- !x! if(table_exists(ups_invalid_old_pks))
	drop table if exists ups_invalid_old_pks cascade;
-- !x! endif
-- !x! if(view_exists(ups_invalid_pk_rwcnt))
	drop view if exists ups_invld_pk_rwcnt cascade;
-- !x! endif
-- !x! if(table_exists(ups_existing_new_pks))
	drop table if exists ups_existing_new_pks cascade;
-- !x! endif
-- !x! if(view_exists(ups_exst_nwpk_rwcnt))
	drop view if exists ups_exst_nwpk_rwcnt cascade;
-- !x! endif
-- !x! if(table_exists(ups_pk_mapping_conflict))
	drop table if exists ups_pk_mapping_conflict cascade;
-- !x! endif
-- !x! if(view_exists(ups_map_conf_rwcnt))
	drop view if exists ups_map_conf_rwcnt cascade;
-- !x! endif
-- !x! if(table_exists(ups_pk_duplicate_keys))
	drop table if exists ups_pk_duplicate_keys cascade;
-- !x! endif
-- !x! if(view_exists(ups_dup_key_rwcnt))
	drop view if exists ups_dup_key_rwcnt cascade;
-- !x! endif
-- !x! if(table_exists(ups_fkcol_refs))
	drop table if exists ups_fkcol_refs cascade;
-- !x! endif
-- !x! if(table_exists(ups_pkcol_deps))
	drop table if exists ups_pkcol_deps cascade;
-- !x! endif
-- !x! if(table_exists(ups_pkfk_ctrl))
	drop table if exists ups_pkfk_ctrl cascade;
-- !x! endif
-- !x! if(view_exists(ups_next_fk))
	drop view if exists ups_next_fk cascade; 
-- !x! endif

-- !x! END SCRIPT
-- ###################  UPDTPKQA_ONE  ########################
-- ################################################################
--			Script UPDTPKQA_ONE_INNERLOOP
-- ----------------------------------------------------------------
-- !x! BEGIN SCRIPT UPDTPKQA_ONE_INNERLOOP with parameters(stage_pfx, qaerror_table, display_errors)
-- !x! if(hasrows(ups_next_fk))

	-- !x! select_sub ups_next_fk
	
	-- Compile FK info for the selected constraint
	-- !x! if(table_exists(ups_sel_fk_cols))
		drop table if exists ups_sel_fk_cols cascade;
	-- !x! endif
	create table ups_sel_fk_cols
	select
		fk_constraint, table_name,
		parent_table, 
		group_concat(parent_column order by column_name separator ', ') as referenced_cols,
		group_concat('s.new_' || column_name || '=' || 'b.' || parent_column order by column_name separator ' and ') as join_condition
	from ups_pkcol_deps
	where fk_constraint='!!@fk_constraint!!'
	group by 
		fk_constraint, table_name,
		parent_table
	;
	-- !x! select_sub ups_sel_fk_cols
	
	-- Construct SQL to check the selected FK
	-- !x! sub ~select_stmt create table ups_pk_fk_check select !!@old_pkcol_aliased!!, !!@new_pkcol!! from !!#stage_pfx!!!!@table_name!! as s
	-- !x! sub ~join_stmt left join !!@parent_table!! as b on !!@join_condition!!
	-- !x! sub ~where_clause where !!@all_newpk_col_not_empty!! and b.!!@any_referenced_column!! is null
	
	-- !x! sub ~fk_check !!~select_stmt!!
	-- !x! sub_append ~fk_check !!~join_stmt!!
	-- !x! sub_append ~fk_check !!~where_clause!!
	
	-- Write the SQL to the log file if requested.
	-- !x! if(sub_defined(logfile))
	-- !x! andif(sub_defined(log_sql))
	-- !x! andif(is_true(!!log_sql!!))
		-- !x! write "" to !!logfile!!
		-- !x! write "SQL for checking foreign key !!@fk_constraint!! for PK update to !!@table_name!!:" to !!logfile!!
		-- !x! write [!!~fk_check!!] to !!logfile!!
	-- !x! endif
	
	-- Run the check
	-- !x! if(table_exists(ups_pk_fk_check))
		drop table if exists ups_pk_fk_check cascade;
	-- !x! endif

	!!~fk_check!!;

	-- !x! if(hasrows(ups_pk_fk_check))
		
		-- !x! if(view_exists(ups_pk_fk_check_rwcnt))
			drop view if exists ups_pk_fk_check_rwcnt cascade;
		-- !x! endif
		create or replace view ups_pk_fk_check_rwcnt as 
		select count(*) as rwcnt
		from ups_pk_fk_check
		;
		
		-- !x! subdata ~rowcount ups_pk_fk_check_rwcnt
		-- !x! sub ~error_description !!@parent_table!! (!!@referenced_cols!!): !!~rowcount!! row(s)
		
		-- !x! write "    Violation of foreign key !!@fk_constraint!! in new primary key columns in !!@stag_pfx!!!!@table_name!! referencing !!@parent_table!!: !!~rowcount!! row(s)"
		
		-- !x! if(view_exists(pk_fk_qa_error))
			drop view if exists ups_pk_fk_qa_error cascade;
		-- !x! endif
		create or replace view ups_pk_fk_qa_error as 
		select
			error_code, error_description
		from !!#qaerror_table!!
		where error_code='Invalid reference to parent table(s)';
		-- !x! if(hasrows(ups_pk_fk_qa_error))
			update !!#qaerror_table!!
			set error_description=error_description || '; !!~error_description!!'
			where error_code='Invalid reference to parent table(s)';
		
		-- !x! else
			insert into !!#qaerror_table!! (error_code, error_description)
			values ('Invalid reference to parent table(s)', '!!~error_description!!')
			;			
		-- !x! endif
		
	
		-- !x! if(sub_defined(logfile))
			-- !x! write "" to !!logfile!!
			-- !x! write "Violation of foreign key !!@fk_constraint!! in new primary key columns in !!@stage_pfx!!!!@table_name!! referencing !!@parent_table!!: !!~rowcount!! row(s)" to !!logfile!!
			-- !x! if(sub_defined(log_errors))
			-- !x! andif(is_true(!!log_errors!!))
				-- !x! export ups_pk_fk_check append to !!logfile!! as txt
			-- !x! endif
		-- !x! endif
		-- !x! if(is_true(!!#display_errors!!))
			-- !x! prompt message "Violation of foreign key !!@fk_constraint!! in  new primary key columns in !!@stage_pfx!!!!@table_name!! referencing !!@parent_table!!" display ups_pk_fk_check
		-- !x! endif	
			
	-- !x! endif

	-- Mark constraint as processed
	update ups_pkfk_ctrl
	set processed=True
	where fk_constraint='!!@fk_constraint!!';

	-- !x! if(table_exists(ups_sel_fk_cols))
		drop table if exists ups_sel_fk_cols cascade;
	-- !x! endif
	-- !x! if(table_exists(ups_pk_fk_check))
		drop table if exists ups_pk_fk_check cascade;
	-- !x! endif
	-- !x! if(view_exists(ups_pk_fk_check_rwcnt))
		drop view if exists ups_pk_fk_check_rwcnt cascade;
	-- !x! endif
	-- !x! if(view_exists(pk_fk_qa_error))
		drop view if exists ups_pk_fk_qa_error cascade;
	-- !x! endif

	--LOOP
	-- !x! execute script updtpkqa_one_innerloop with (stage_pfx=!!#stage_pfx!!, qaerror_table=!!#qaerror_table!!, display_errors=!!#display_errors!!)
	
-- !x! endif

-- !x! END SCRIPT
-- ####################  End of UPDTPKQA_ONE  ########################
-- ################################################################

