Archive for the ‘sql’ Category
MySQL’s List Partition Key
While reviewing some material and explaining list partitioning in MySQL, I heard a rumor that sounded false. The rumor was that you can’t partition on anything other than the primary key column. That’s untrue, you can partition on another column provided it’s an integer column.
It appears the culprit that led to the rumor is a misunderstanding around ERROR 1503
, which returns the following:
ERROR 1503 (HY000): A PRIMARY KEY must include ALL COLUMNS IN the TABLE's partitioning function |
The problem can be recreated by defining a table with an inline primary key constraint instead of an INDEX
on the auto incrementing column. Here’s an example of how to create the error message:
1 2 3 4 5 6 7 8 9 10 11 | CREATE TABLE franchise ( franchise_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT , franchise_number INT UNSIGNED , franchise_name VARCHAR(20) , city VARCHAR(20) , state VARCHAR(20)) PARTITION BY LIST(franchise_number) ( PARTITION offshore VALUES IN (49,50) , PARTITION west VALUES IN (34,45,48) , PARTITION desert VALUES IN (46,47) , PARTITION rockies VALUES IN (38,41,42,44)); |
The table should be defined without the inline PRIMARY KEY
constraint on the auto incrementing column when you want to partition on another column. An index on the auto incrementing column doesn’t conflict with primary key rules. The correct syntax for a list partition would be:
1 2 3 4 5 6 7 8 9 10 11 12 | CREATE TABLE franchise ( franchise_id INT UNSIGNED AUTO_INCREMENT , franchise_number INT UNSIGNED , franchise_name VARCHAR(20) , city VARCHAR(20) , state VARCHAR(20) , INDEX idx (franchise_id)) engine=innodb PARTITION BY LIST(franchise_number) ( PARTITION offshore VALUES IN (49,50) , PARTITION west VALUES IN (34,45,48) , PARTITION desert VALUES IN (46,47) , PARTITION rockies VALUES IN (38,41,42,44)); |
The same problem can manifest itself on range partitioning. The fix is the same and here’s a code example:
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE ordering ( ordering_id INT UNSIGNED AUTO_INCREMENT , item_id INT UNSIGNED , rental_amount DECIMAL(15,2) , rental_date DATE , INDEX idx (ordering_id)) PARTITION BY RANGE(item_id) ( PARTITION jan2011 VALUES LESS THAN (10000) , PARTITION feb2011 VALUES LESS THAN (20000) , PARTITION mar2011 VALUES LESS THAN (30000)); |
The MySQL 5.6 Partitioning Keys, Primary Keys, and Unique Keys article is the key reference. It’s unfortunate that MySQL can’t partition on something other than an integer with PARTITION BY LIST
syntax (check the changes in MySQL 5.6 online documentation). As you’ll notice in my more recent comment back to Tim, you can when you change the SQL phrase to PARTITION BY LIST COLUMNS
. MySQL now has another feature that the Oracle Database 11g supports, that is list partitioning on variable-length string columns.
There are a few required changes but here’s a working example. The index must work against the auto incrementing column. The example comes from my reply to Tim, who raised a great question.
CREATE TABLE franchise ( franchise_id INT UNSIGNED AUTO_INCREMENT , franchise_number INT UNSIGNED , franchise_name VARCHAR(20) , city VARCHAR(20) , state VARCHAR(20) , KEY idx (franchise_id)) engine=innodb PARTITION BY LIST COLUMNS (franchise_name) ( PARTITION m VALUES IN ('McDonald','Maggiano') , PARTITION p VALUES IN ('Pappa John','Pizza Hut') , PARTITION t VALUES IN ('Taco Bell','Taco Time')); |
While that works, the better approach creates two unique constraints. One unique constraint on the natural key of the franchise_number
and franchise_name
, which serves to optimize access to data sets looking data up in the table based on a natural key search. Another unique constraint on the surrogate (auto incrementing column) and the two natural key columns, which optimizes joins from foreign keys to the partitioned table’s primary key.
CREATE TABLE franchise ( franchise_id INT UNSIGNED AUTO_INCREMENT , franchise_number INT UNSIGNED , franchise_name VARCHAR(20) , city VARCHAR(20) , state VARCHAR(20) , UNIQUE ink (franchise_number, franchise_name) , UNIQUE enk (franchise_id, franchise_number, franchise_name)) engine=innodb PARTITION BY LIST COLUMNS (franchise_name) ( PARTITION m VALUES IN ('McDonald','Maggiano') , PARTITION p VALUES IN ('Pappa John','Pizza Hut') , PARTITION t VALUES IN ('Taco Bell','Taco Time')); |
Somebody asked for a composite partitioning (MySQL terminology is subpartitioning) example, so here it is:
1 2 3 4 5 6 7 8 9 10 11 12 | CREATE TABLE ordering ( ordering_id INT UNSIGNED AUTO_INCREMENT , item_id INT UNSIGNED , store_id INT UNSIGNED , rental_amount DECIMAL(15,2) , rental_date DATE , INDEX idx (ordering_id)) PARTITION BY RANGE(item_id) SUBPARTITION BY HASH(store_id) SUBPARTITIONS 4 ( PARTITION jan2011 VALUES LESS THAN (10000) , PARTITION feb2011 VALUES LESS THAN (20000) , PARTITION mar2011 VALUES LESS THAN (30000)); |
Hope this helps those researching ERROR 1503
on list or range partitioning.
MySQL Virtual Columns?
While preparing for next week’s classes, I ran across a new future feature of MySQL – the virtual column. It appears, according to the article, that MySQL will have virtual columns in MySQL 6.
The syntax is virtually identical to that found in Oracle databases. For example, here’s what suppose to work in the future:
1 2 3 4 5 | CREATE TABLE salary ( salary_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT , salary DOUBLE NOT NULL , bonus DOUBLE , compensation DOUBLE AS (salary + bonus)); |
It’s virtually (excuse the pun) how it would work in Oracle Database 11g. I wonder what else we see move over into MySQL. Just a note, virtual columns moved from Oracle RDB (a prior acquisition from Digital Equipment Corporation in 1996) to the Oracle Database 11g.
Equivalent syntax in Oracle Database 11g would be like this for concatenation:
1 2 3 4 5 | CREATE TABLE employee ( employee_id NUMBER , first_name VARCHAR2(20) , last_name VARCHAR2(20) , full_name VARCHAR2(41) AS (first_name || ' ' || last_name)); |
And like this for a math operation (like the one noted above for a future release of MySQL):
1 2 3 4 5 | CREATE TABLE salary ( salary_id NUMBER CONSTRAINT pk_salary PRIMARY KEY , salary NUMBER(15,2) CONSTRAINT nn_salary_01 NOT NULL , bonus NUMBER(15,2) , compensation NUMBER(15,2) AS (salary + bonus)); |
Adding NOT NULL constraint
Somebody wanted to know if you could add a NOT NULL
column constraint in MySQL. That’s a great question and the answer is yes. The following example shows you how to do it.
- Create a sample table without a
NOT NULL
constraint on a column that should have one. After creating this table, describe it and you’ll see that thetesting_text
column is
CREATE TABLE testing ( testing_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY , testing_text VARCHAR(10)); |
- Change the column definition from null allowed to not null for the
TESTING_TEXT
column. The only problem with this syntax is that it only works when there are no null values in the table or there are no rows in the table.
ALTER TABLE testing CHANGE testing_text testing_text VARCHAR(10) NOT NULL; |
- Change the column definition from not null constrained to null allowed for the
TESTING_TEXT
column.
ALTER TABLE testing CHANGE testing_text testing_text VARCHAR(10); |
As always, I hope this helps.
Drop Object If Exists
Writing an anonymous block to conditionally drop tables and sequences got very old. I figured it was time to simply put a procedure that would simplify it. Avoiding repetition was important too, so it supports an invoker rights model. This mimics the IF EXISTS
syntax available in MySQL.
You need to create this as the SYSTEM
user and then grant execute permission on the procedure to PUBLIC
. Then, you need to create a couple public synonyms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | -- Create a procedure to simplify dropping objects. CREATE OR REPLACE PROCEDURE drop_ifexists ( pv_type VARCHAR2 , pv_table VARCHAR2 ) AUTHID CURRENT_USER IS -- String for DDL command. sql_text VARCHAR2(2000); -- Declare a parameterized cursor. CURSOR find_object ( cv_type VARCHAR2 , cv_table VARCHAR2 ) IS SELECT uo.object_name , uo.object_type FROM user_objects uo WHERE uo.object_name = UPPER(cv_table) AND uo.object_type = UPPER(cv_type); BEGIN -- Open the cursor with the input variables. FOR i IN find_object(pv_type, pv_table) LOOP -- Check for a table object and append cascade constraints. IF i.object_type = 'TABLE' THEN sql_text := 'DROP '||i.object_type||' '||i.object_name||' CASCADE CONSTRAINTS'; ELSE sql_text := 'DROP '||i.object_type||' '||i.object_name; END IF; -- Run dynamic command. EXECUTE IMMEDIATE sql_text; END LOOP; END drop_ifexists; / |
After creating the procedure in the SYSTEM
user schema, you should run these Data Control Language (DCL) commands:
GRANT EXECUTE ON drop_ifexists TO PUBLIC; CREATE PUBLIC SYNONYM drop_ifexists FOR system.drop_ifexists; |
Assuming you have a table named MESSAGE
, you can call the drop_ifexists
procedure to conditionally delete it as follows below. The same procedure works with any object in a user’s schema.
EXECUTE drop_ifexists('table','message'); |
The only problem comes with types because they can have dependents. When they have dependents you can’t drop them until you’ve removed all the dependents. There are two types of dependents types and collection types.
Since discovering those may be difficult for new users, here’s another invoker rights function to discover user defined types. You should note that you’ll need to create the types before you try to compile the code (create the stored program in the Oracle database).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | -- Create a SQL structure. CREATE OR REPLACE TYPE dependent_type_obj IS OBJECT ( dependent_type VARCHAR2(10) , type_name VARCHAR2(30)); / -- Create a SQL collection of a user-defined data structure. CREATE OR REPLACE TYPE dependent_type_tab IS TABLE OF dependent_type_obj; / -- Create a procedure to discover type dependents. CREATE OR REPLACE FUNCTION type_dependents ( pv_type_name VARCHAR2 ) RETURN dependent_type_tab AUTHID CURRENT_USER IS -- Declare a counter for the collection variable. c NUMBER := 1; -- Declare a return type variable. list DEPENDENT_TYPE_TAB := dependent_type_tab(); -- Declare a parameterized cursor for dependent types. CURSOR find_dependent_types ( cv_type VARCHAR2 ) IS SELECT 'STANDALONE' AS dependent_type , at.type_name FROM all_types at INNER JOIN all_type_attrs ata ON at.type_name = ata.type_name WHERE at.owner = USER AND ata.attr_type_name = UPPER(cv_type) UNION ALL SELECT 'COLLECTION' AS dependent_type , act.type_name FROM all_types at INNER JOIN all_coll_types act ON at.type_name = act.elem_type_name WHERE at.owner = USER AND act.elem_type_name = UPPER(cv_type); BEGIN -- Loop through all returns and add them to the return collection. FOR i IN find_dependent_types(pv_type_name) LOOP list.EXTEND; list(c) := dependent_type_obj(i.dependent_type, i.type_name); c := c + 1; END LOOP; -- Return the list. RETURN list; END type_dependents; / |
You should also make the following grants and synonyms:
GRANT EXECUTE ON system.type_dependents TO PUBLIC; CREATE PUBLIC SYNONYM type_dependents FOR system.type_dependents; GRANT ALL ON system.dependent_type_obj TO PUBLIC; GRANT ALL ON system.dependent_type_tab TO PUBLIC; |
Then, you can call the type_dependents
function to find any dependent user-defined data types. If so, you must drop them from the bottom or most specialized straight up the type tree to the root node data type.
SELECT * FROM TABLE(type_dependents('&type_name')); |
Why SELECT-INTO, eh?
Somebody raised the question about writing stored functions while we were discussing scalar subqueries against COMMON_LOOKUP
tables. Common look up tables store collections of possible <OPTION>
elements. They’re basically generalized tables that contain a set of smaller tables, where the row sets make up a list of unique values.
When you write a function that must return one and only one row, the SELECT-INTO
syntax is ideal in Oracle because it automatically raises an exception when the query returns no row or two or more rows. MySQL doesn’t raise an automatic exception when a SELECT-INTO
fails to return a row but it does raise an ERROR 1722
when two or more rows are found.
Here are sample implementations written in Oracle’s PL/SQL and MySQL’s SQL/PSM languages. If you’re an Oracle developer and new to MySQL, the biggest oddity may be the delimiter, or it may be the sizing of formal parameters. Hopefully, its not the lack of formal declaration and exception blocks in SQL/PSM. For those coming from MySQL, the OR REPLACE
command or dynamic sizing of formal parameter list values are the big changes.
Oracle PL/SQL Function
The Oracle PL/SQL function is very straightforward and helped by pre-defined exceptions for both the no data found and too many rows found errors. Formal parameters inherit their physical size at run time from the calling scope program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | -- Create or replace existing function with new implementation. CREATE OR REPLACE FUNCTION get_common_lookup_id ( pv_table VARCHAR2 , pv_column VARCHAR2 , pv_type VARCHAR2 ) RETURN NUMBER IS -- Declare a return variable. lv_return NUMBER; BEGIN -- Query data and assign it to a local variable. SELECT common_lookup_id INTO lv_return FROM common_lookup WHERE common_lookup_table = pv_table AND common_lookup_column = pv_column AND common_lookup_type = pv_type; -- Return the value found. RETURN lv_return; EXCEPTION -- Handle errors. WHEN NO_DATA_FOUND THEN RAISE; WHEN TOO_MANY_ROWS THEN RAISE; END; / |
MySQL SQL/PSM Function
The lack of an implicitly raised exception when a SELECT-INTO
structure fails to return a row adds several steps to a SQL/PSM function. You must define a custom exception and an if-block to raise the exception, which is trigger by returning a null value into the lv_return
local variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /* Conditionally drop the function before trying to create it. */ DROP FUNCTION IF EXISTS get_common_lookup_id; /* Change the delimiter to write a stored procedure into the database. */ DELIMITER $$ /* Create a function that replaces a scalar subquery. */ CREATE FUNCTION get_common_lookup_id ( pv_table VARCHAR(30) , pv_column VARCHAR(30) , pv_type VARCHAR(30)) RETURNS INT UNSIGNED BEGIN /* Declare a return variable. */ DECLARE lv_return INT UNSIGNED; /* Declare a local variable for a subsequent handler. */ DECLARE no_data_found CONDITION FOR SQLSTATE '99001'; /* Query data and assign it to a local variable. */ SELECT common_lookup_id INTO lv_return FROM common_lookup WHERE common_lookup_table = pv_table AND common_lookup_column = pv_column AND common_lookup_type = pv_type; /* The local variable is only null when no rows are returned by the implicit cursor. */ IF lv_return IS NULL THEN SIGNAL no_data_found SET MESSAGE_TEXT = 'Result was no rows found.'; END IF; /* Return the value found. */ RETURN lv_return; END; $$ /* Reset the delimiter to the default to run other programs. */ DELIMITER ; |
Hope this help solve your problems.
PHP Database Authentication
A few years ago I wrote a couple articles showing how to use PHP to connect to an Oracle Database 10g Express Edition instance. They’re still there on Oracle’s Technical Network but the source files are missing. It appears that Oracle may have migrated the articles to a new server but failed to migrate the source files.
Don’t forget that you’ll need to Install the Zend Community Server. Then, you need to create an IDMGMT1
, IDMGMT2
, and IDMGMT3
users and run the create_identity_db2.sql
or create_identity_db3.sql
seeding script for both database validation models. You’ll find links to the original articles, the source code in zip files. The newer version with CSS is the IDMGMT3
code.
The CSS sign-on form looks like this:
The portal page looks like this:
The add new user page looks like this:
The source files are as follows:
- Database Authentication #1 – IdMgmt1.zip
- Database Authentication #2 – IdMgmt2.zip
- Database Authentication #3 – IdMgmt3.zip includes the CSS but works with the same write-up as Idmgmt2.
Hope this helps those who wanted the files.
Oracle Legacy Workaround
We had a discussion today about how you can manage legacy code that you can’t change. For example, how do you access a PL/SQL function in PHP that returns a PL/SQL table of record structures? PL/SQL tables, index-by tables, or associate arrays are one in the same dependent on the release documentation. They’ve been available since Oracle 7.3 (roughly 15+ years).
You’ve a handful of solutions but I think the best is to wrap it in a Pipelined Table function (more on that in this older post). Here’s an example of such a package, wrapper function, and PHP program calling the wrapper function (command-line only PHP sample code).
Let’s say you have the following type of legacy package specification and body:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | -- Create the package specification. CREATE OR REPLACE PACKAGE lib IS /* Define a record structure. */ TYPE movie_title_record IS RECORD ( title VARCHAR2(60) , subtitle VARCHAR2(60)); /* Define an associative array of a package record structure. */ TYPE movie_title_table IS TABLE OF movie_title_record INDEX BY BINARY_INTEGER; /* Define a prototype of a package function. */ FUNCTION get_movie ( pv_title VARCHAR2 ) RETURN lib.movie_title_table; END lib; / -- Create the package body. CREATE OR REPLACE PACKAGE BODY lib IS /* Implement the package function. */ FUNCTION get_movie ( pv_title VARCHAR2 ) RETURN lib.movie_title_table IS /* Declare a counter variable. */ lv_counter INTEGER := 1; /* Declare an instance of the package nested table and initialize it. */ lv_table LIB.MOVIE_TITLE_TABLE := lib.movie_title_table(); /* Define a parameterized cursor to read values from the ITEM table. */ CURSOR c ( cv_partial_title VARCHAR2 ) IS SELECT i.item_title , i.item_subtitle FROM item i WHERE i.item_title LIKE '%'||cv_partial_title||'%'; BEGIN /* Read the contents of the parameterized cursor. */ FOR i IN c (pv_title) LOOP /* Extend space, assign values from the cursor to the record structure of the nested table, and increment counter. */ lv_table.EXTEND; lv_table(lv_counter) := i; lv_counter := lv_counter + 1; END LOOP; /* Return PL/SQL-scope nested table. */ RETURN lv_table; END get_movie; END lib; / |
You can wrap the lib
package’s get_movie
function with a schema-level function provided you convert the older associative array to a PL/SQL-scope nested table. You can do that in two steps. The first requires that you create a wrapper package specification, like the following example. The second step requires you to write a conversion wrapper function, shown later.
The table is dependent on the named record structure from the lib
, and as such the packages are now tightly coupled. This is not uncommon when you can’t fix a vendors legacy code set.
1 2 3 4 5 6 7 | CREATE OR REPLACE PACKAGE wlib IS /* Define a nested table of a package record structure. */ TYPE movie_title_table IS TABLE OF lib.movie_title_record; END wlib; / |
The wrapper function also converts the Oracle Database 7.3 forward data type to an Oracle Database 8.0.3 data type, and then pipes it into a SQL aggregate table. SQL aggregate tables are valid call parameters in the SQL-context. The TABLE
function converts the collection of record structures into an inline view or derived table, as you’ll see a little farther along.
You should note that the return type of this function differs from the original package-level get_movie
function. The former uses an associative array defined in the lib
, while the latter uses a nested table defined in the wlib
package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | CREATE OR REPLACE FUNCTION get_movie ( pv_title VARCHAR2 ) RETURN wlib.movie_title_table PIPELINED IS /* Define a PL/SQL-scope associative array (Available Oracle 7.3). */ lv_table_source LIB.MOVIE_TITLE_TABLE; /* Define a PL/SQL-scope nested table (Available Oracle 8.0.3). */ lv_table_target WLIB.MOVIE_TITLE_TABLE := wlib.movie_title_table(); BEGIN /* Assign the results of a PL/SQL-scope call to the package function. */ lv_table_source := lib.get_movie(dbms_assert.simple_sql_name(pv_title)); /* Read the contents of the PL/SQL-scope nested table into a PIPE ROW. */ FOR i IN 1..lv_table_source.COUNT LOOP lv_table_target.EXTEND; lv_table_target(i) := lv_table_source(i); PIPE ROW(lv_table_target(i)); END LOOP; END; / |
You can test this exclusively in SQL*Plus with the following formatting and query. The TABLE
function translates the returned array into an inline view or derived table for processing.
-- Format columns for display with SQL*Plus. COLUMN title FORMAT A20 HEADING "Movie Title" COLUMN subtitle FORMAT A20 HEADING "Movie Subtilte" -- Select the contents of the schema-level function in a SQL-context. SELECT * FROM TABLE(get_movie('Star')); |
If you’re using my sample code from the Oracle Database 11g PL/SQL Programming book, you should see:
Movie Title Movie Subtilte -------------------- -------------------- Star Wars I Phantom Menace Star Wars II Attack of the Clones Star Wars II Attack of the Clones Star Wars III Revenge of the Sith |
The following is a simple command-line PHP program that calls the wrapper function. It calls the wrapper function, which calls the lib.get_movie()
function, and it converts the PL/SQL data type from an associative array (Oracle 7.3+ data type) to a nested table (Oracle 8.0.3+ data type). The nested table is defined in the wlib
library, which supplements rather than replaces the original lib
library.
The last thing that the wrapper function does is transform the associative array result into a nested table before placing it in the pipe (this process is known as a Pipelined Table function). Only nested table and varray data types may be piped into a SQL aggregate table. Then, the external programming language can manage the output as if it were a query.
Here’s the PHP program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | <?php // Connect to the database. if ($c = oci_connect("student","student","localhost/orcl")) { // Parsing a statement into a digestable SQL statement. $s = oci_parse($c,"SELECT * FROM TABLE(get_movie('Star'))"); // Execute the parsed statement. oci_execute($s,OCI_DEFAULT); // Store control variable for the number of columns returned by the statement. $columns = oci_num_fields($s); // Find the number of columns, loop through them, and write their column name metadata. for ($i = 1; $i <= $columns; $i++) { // Print the column names, also known as field names. print oci_field_name($s,$i); // Define a variable. $line; /* Check whether a variable is declared and pad it. * The numeric literal is for my convenience because the maximum size * of possible returns is known. In a real situation, you'd use the * following str_pad() call: * * str_pad($temp,oci_field_size($s,$i),"-") */ if (!isset($line)) $line .= str_pad($temp,15,"-"); else $line .= " ".str_pad($temp,20,"-"); /* One of the values requires a double tab to line up, otherwise this practice is unwise. */ if ($i < $columns) print "\t\t"; } // Print line return for the header and a line to mimic Oracle SQL*Plus output. print "\n"; print $line .= "\n"; // Process row-by-row data returned when data is returned. while (oci_fetch($s)) { // Process column-by-column data returned for each row. for ($i = 1; $i <= $columns; $i++) { print oci_result($s,$i); if ($i < $columns) print "\t"; } // Print line return for the row of data returned. print "\n"; } // Release resources. oci_close($c); // Explicitly free any resources. oci_free_statement($s); oci_free_cursor($c); } ?> |
Assuming you call this callOracle.php
, you can call it from the command-line with this syntax:
php callOracle.php |
It prints, like it would in SQL*Plus:
TITLE SUBTITLE --------------- -------------------- Star Wars I Phantom Menace Star Wars II Attack of the Clones Star Wars II Attack of the Clones Star Wars III Revenge of the Sith |
Hope that helps those working with legacy Oracle code.
Two-stepping Sequences
Sometimes I’m amazed at things that come up. A student wondered why the sequences were incrementing by two when they’re defined to increment by one. It turns out that they were using Oracle APEX to create SQL statements to build a table, constraints, and a auto-numbering sequence trigger. Before executing the code, they’d copy it into their re-runnable script that created their schema.
Here’s an example of code that was generated by APEX for a table:
-- Create the table. CREATE TABLE onesy ( onesy_id NUMBER , onesy_text VARCHAR2(20)); -- Add the primary key constraint. ALTER TABLE onesy ADD CONSTRAINT onesy_seq PRIMARY KEY (onesy_id); -- Add a database trigger. CREATE OR REPLACE TRIGGER onesy_trg BEFORE INSERT ON onesy FOR EACH ROW BEGIN :NEW.onesy_id := onesy_s1.NEXTVAL; END; / |
This works in APEX because it doesn’t create forms that call onesy_seq.NEXTVAL
but they did create that logic in their forms. The INSERT
statement would look like:
INSERT INTO onesy VALUES (onesy_seq.NEXTVAL, 'One'); |
Therefore, the INSERT
statement incremented the trigger by one and the database trigger incremented it by one. The result is that sequences two-step, which isn’t effective or the desired behavior.
After I explained the two-step problem, they asked if they could only call the trigger when the primary key value was null. While they could do that like this:
1 2 3 4 5 6 7 8 9 | -- Add a database trigger. CREATE OR REPLACE TRIGGER onesy_trg BEFORE INSERT ON onesy FOR EACH ROW WHEN (NEW.one_id IS NULL) BEGIN :NEW.onesy_id := onesy_s1.NEXTVAL; END; / |
The problem is that this type of trigger doesn’t stop other possible problems. While it prevents two-stepping the sequence, it doesn’t prevent two other errors.
One possible error that isn’t managed in this scenario is the use of numeric literals beyond the next value of the sequence. It writes the row but eventually the sequence catches up to the higher value and a production insert would fail. It would raise the following exception.
INSERT INTO onesy (onesy_text) VALUES ('Eight') * ERROR at line 1: ORA-00001: UNIQUE CONSTRAINT (STUDENT.PK_ONE) violated |
Another possible error can occur when you use a bulk insert operation. Assuming you’re inserting 500 rows at a go, you query the maximum value of the onesy_id
column and then create an array of 500 numbers. Then, you perform the bulk INSERT
statement. The next call to the trigger would raise another ORA-00001
unique constraint error.
Yes, you could lock the table before you perform the bulk operation. After the bulk operation you would drop and recreate the sequence with a new value equal to the maximum value in the column, and unlock the table. This limits concurrency of operation. You could treat these bulk operations as off-line transactions (batch processing) and it would work nicely.
You could also implement a policy that no bulk operations provide generated column values that link to a sequence. Beyond it’s impracticality to manage, that type of restriction does limit the benefit of bulk operations.
The students wanted a solution. So, here’s my take on a trigger that prevents collision with values above the next sequence value. It assumes that bulk operations will be performed as batch processing where you can disable this trigger.
This trigger disallows numeric literals, logs any attempts to use them, and stops processing when an INSERT
statement tries to use anything other than the .NEXTVAL
of the sequence. It will only work in an Oracle Database 11g database because the context of using a sequence_name.CURRVAL
in a comparison isn’t supported in prior releases. The onesy
table is renamed the one
table in the example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | -- Create a sequence for table ONE that starts with 1 and increments by 1. CREATE SEQUENCE msg_s1; CREATE OR REPLACE TRIGGER one_t1 BEFORE INSERT ON one FOR EACH ROW DECLARE /* Define an autonomous transaction scope to the trigger. */ PRAGMA AUTONOMOUS_TRANSACTION; /* Declare a local exception raised when a .CURRVAL pseudo column for a sequence is called before a .NEXTVAL for the same sequence in the same session. */ no_sequence_in_scope EXCEPTION; PRAGMA EXCEPTION_INIT(no_sequence_in_scope,-08002); BEGIN /* Check if surrogate key is provided and the sequence not out of transaction scope. */ IF :NEW.one_id IS NOT NULL AND NOT :NEW.one_id = one_s1.CURRVAL THEN /* Write message when sequence value is a numeric literal not a sequence generated value but a one_s1.NEXTVAL was previously called in the session. Commit after write or information is lost because it throws an user-defined exception. */ INSERT INTO msg VALUES (msg_s1.NEXTVAL,'ID value less or greater than .NEXTVAL ['||:NEW.one_id||']['||:NEW.one_text||'].'); COMMIT; /* Stop processing by throwing exception. */ RAISE_APPLICATION_ERROR(-20002,'ID provided by calling scope is not next sequence value ['||:NEW.one_id||']['||:NEW.one_text||'].'); ELSIF :NEW.one_id = one_s1.CURRVAL THEN /* Do nothing, calling scope is correct with a one_s1.NEXTVAL sequence call. */ NULL; ELSE /* Increment sequence and assign a value when one isn't provided, like a NULL value. */ :NEW.one_id := one_s1.NEXTVAL; END IF; EXCEPTION /* Handle a no sequence in scope error. */ WHEN no_sequence_in_scope THEN /* Write and commit log message for error. */ INSERT INTO msg VALUES (msg_s1.NEXTVAL,'ID provided by calling scope is invalid ['||:NEW.one_id||']['||:NEW.one_text||'].'); COMMIT; /* Stop processing by throwing an exception. */ RAISE_APPLICATION_ERROR(-20001,'Not a sequence generated value ['||:NEW.one_id||'].'); END; / |
Since anonymous transaction triggers are tricky, it’s important to note that the message writing requires two commits. One before raising the exception when the .CURRVAL
is in session scope and another in the exception handler before raising the error. If you forget those COMMIT
statements, this is a sample of the error stack:
INSERT INTO one VALUES (one_s1.nextval,'Six') * ERROR at line 1: ORA-06519: active autonomous TRANSACTION detected AND rolled back ORA-06512: at "STUDENT.ONE_T1", line 31 ORA-04088: error during execution OF TRIGGER 'STUDENT.ONE_T1' |
The trigger raises the following type of exceptions for an offending INSERT
statement. The first occurs when the sequence is valid in the session scope, like:
DECLARE * ERROR at line 1: ORA-20001: NOT a SEQUENCE generated VALUE [1]. ORA-06512: at "STUDENT.ONE_T1", line 48 ORA-04088: error during execution OF TRIGGER 'STUDENT.ONE_T1' ORA-06512: at line 15 |
The second occurs when the sequence isn’t valid in the session scope.
INSERT INTO one VALUES (401,'Nine') * ERROR at line 1: ORA-20002: ID provided BY calling scope IS NOT NEXT SEQUENCE VALUE [401][Nine]. ORA-06512: at "STUDENT.ONE_T1", line 24 ORA-04088: error during execution OF TRIGGER 'STUDENT.ONE_T1' |
A value that’s below the current high-watermark of the sequence raises a unique constraint, like this:
INSERT INTO one VALUES (1,'Eight') * ERROR at line 1: ORA-00001: UNIQUE CONSTRAINT (STUDENT.PK_ONE) violated |
The following is a script with all the necessary code components to test the example.
Oracle Script ↓
Unfold this if you’d like to see the complete test case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | -- Conditionally drop tables and sequences. BEGIN FOR i IN (SELECT object_name, object_type FROM user_objects WHERE object_name IN ('ONE','ONE_S1','MSG','MSG_S1')) LOOP IF i.object_name = 'TABLE' THEN EXECUTE IMMEDIATE 'DROP '||i.object_type||' '||i.object_name||' CASCADE CONSTRAINTS'; ELSE EXECUTE IMMEDIATE 'DROP '||i.object_type||' '||i.object_name; END IF; END LOOP; END; / -- Create a table ONE. CREATE TABLE one (one_id NUMBER CONSTRAINT pk_one PRIMARY KEY, one_text VARCHAR2(20)); -- Create a sequence for table ONE that starts with 1 and increments by 1. CREATE SEQUENCE one_s1; -- Create a table MSG. CREATE TABLE msg (msg_id NUMBER, msg_text VARCHAR2(80)); -- Create a sequence for table ONE that starts with 1 and increments by 1. CREATE SEQUENCE msg_s1; -- Create or replace trigger. CREATE OR REPLACE TRIGGER one_t1 BEFORE INSERT ON one FOR EACH ROW DECLARE /* Define an autonomous transaction scope to the trigger. */ PRAGMA AUTONOMOUS_TRANSACTION; /* Declare a local exception raised when a .CURRVAL pseudo column for a sequence is called before a .NEXTVAL for the same sequence in the same session. */ no_sequence_in_scope EXCEPTION; PRAGMA EXCEPTION_INIT(no_sequence_in_scope,-08002); BEGIN /* Check if surrogate key is provided and the sequence not out of transaction scope. */ IF :NEW.one_id IS NOT NULL AND NOT :NEW.one_id = one_s1.CURRVAL THEN /* Write message when sequence value is a numeric literal not a sequence generated value but a one_s1.NEXTVAL was previously called in the session. Commit after write or information is lost because it throws an user-defined exception. */ INSERT INTO msg VALUES (msg_s1.NEXTVAL,'ID value less or greater than .NEXTVAL ['||:NEW.one_id||']['||:NEW.one_text||'].'); COMMIT; /* Stop processing by throwing exception. */ RAISE_APPLICATION_ERROR(-20002,'ID provided by calling scope is not next sequence value ['||:NEW.one_id||']['||:NEW.one_text||'].'); ELSIF :NEW.one_id = one_s1.CURRVAL THEN /* Do nothing, calling scope is correct with a one_s1.NEXTVAL sequence call. */ NULL; ELSE /* Increment sequence and assign a value when one isn't provided, like a NULL value. */ :NEW.one_id := one_s1.NEXTVAL; END IF; EXCEPTION /* Handle a no sequence in scope error. */ WHEN no_sequence_in_scope THEN /* Write and commit log message for error. */ INSERT INTO msg VALUES (msg_s1.NEXTVAL,'ID provided by calling scope is invalid ['||:NEW.one_id||']['||:NEW.one_text||'].'); COMMIT; /* Stop processing by throwing an exception. */ RAISE_APPLICATION_ERROR(-20001,'Not a sequence generated value ['||:NEW.one_id||'].'); END; / -- Define a schema collection of strings. CREATE OR REPLACE TYPE one_index AS TABLE OF VARCHAR2(20); / -- Define a schema collection of strings. CREATE OR REPLACE TYPE one_list AS TABLE OF VARCHAR2(20); / -- Anonymous block bulk testing program. DECLARE -- Declare two scalar collection variables. lv_index ONE_INDEX := one_index(); lv_list ONE_LIST := one_list('One','Two','Three','Four','Five'); BEGIN -- Initialize the collection of the ONE_INDEX collection. lv_index.EXTEND(lv_list.COUNT); FOR i IN lv_list.FIRST..lv_list.LAST LOOP dbms_output.put_line('['||i||']['||lv_list(i)||']'); lv_index(i) := i; END LOOP; -- Perform a trivial example of a bulk insert. FORALL i IN lv_list.FIRST..lv_list.LAST INSERT INTO one VALUES (lv_index(i),lv_list(i)); END; / -- Testing INSERT statements until sequence value increments beyond bulk insert values. INSERT INTO one VALUES (6,'Six'); INSERT INTO one VALUES (one_s1.nextval,'Seven'); INSERT INTO one VALUES (1,'Eight'); INSERT INTO one VALUES (401,'Nine'); INSERT INTO one VALUES (NULL,'Ten'); COLUMN msg_id FORMAT 999 HEADING "Msg|ID" COLUMN msg_text FORMAT A72 HEADING "Msg Text" -- SELECT * FROM one; SELECT * FROM msg; -- Anonymous block bulk testing program. DECLARE -- Declare two scalar collection variables. lv_list ONE_LIST := one_list('Twenty-one','Twenty-two','Twenty-three','Twenty-four','Twenty-five'); BEGIN -- Perform a trivial example of a bulk insert. FORALL i IN lv_list.FIRST..lv_list.LAST INSERT INTO one VALUES (NULL,lv_list(i)); END; / -- Query tables. SELECT * FROM one; SELECT * FROM msg; |
If I’ve fat fingered any typing or made logical errors, please let me know.
Correlated Update Statement
My students wanted some additional examples on correlated update statements. I’ve been struggling with what the right way may be to illustrate them.
Correlated subqueries are a hard concept to explain to those new to SQL. While correlated update statements seem impossibly obscure to many or inordinately complex. New SQL developers often flee to the comfort of procedural programs when it comes to update statements.
This uses my video store data model. It’s a correlated update statement to clean up potential corrupt data. More or less something a DBA might run to ensure a business rule hasn’t been violated over time. It checks for the correct foreign key value in a table when a dependent table contains one or more than one row of data.
The aqua-green box highlights a subquery that aggregates foreign key columns and groups the result with the foreign key value. The results from this subquery become a run-time view or derived table. The result set is a foreign key value and a substitute string literal value for each row in the contact table. These results correlate to the update statement’s rows based on the input parameter. The input parameter is a column from each updated row.
A unique key (or check constraint) exists on the combination of the common_lookup_table, common_lookup_column, and common_lookup_type columns. This ensures that only one row is returned and assigned to the member_type column in the member table. The update statement naturally works in either Oracle or MySQL without any porting changes.
Correlated Update Statement ↓
Expand this section to see the clear text for the foregoing image.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | UPDATE member m SET member_type = (SELECT common_lookup_id FROM common_lookup WHERE common_lookup_table = 'MEMBER' AND common_lookup_column = 'MEMBER_TYPE' AND common_lookup_type = (SELECT dt.member_type FROM (SELECT c.member_id , CASE WHEN COUNT(c.member_id) > 1 THEN 'GROUP' ELSE 'INDIVIDUAL' END AS member_type FROM contact c GROUP BY c.member_id) dt WHERE dt.member_id = m.member_id)); |
While this type of solution is powerful in its own right, I thought it might be interesting to see their procedural equivalents. These correlated subqueries run for each row returned by the master query (or outermost statement). Therefore, they act like functions.
Procedural equivalents (or user-defined functions) simplify the update statement like so:
UPDATE member m SET member_type = get_member_type(m.member_id); |
If you’re interested in seeing how you would implement this solution in a user-defined function, just expand the dropdown that interest you.
Oracle User-Defined Function (UDF) ↓
Expand this section to see how to map this logic to a PL/SQL schema-level function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | CREATE OR REPLACE FUNCTION get_member_type (pv_member_id NUMBER) RETURN NUMBER IS -- Define a collection of strings. TYPE type_options IS TABLE OF VARCHAR2(10); -- Declare local variables. lv_dependent NUMBER := 1; lv_return_id NUMBER; -- Declare a local variable of the collection. lv_member_type TYPE_OPTIONS := type_options('INDIVIDUAL','GROUP'); -- Define a dynamic cursor to count the number of foreign key values. CURSOR count_contact (cv_member_id NUMBER) IS SELECT COUNT(c.member_id) FROM contact c WHERE c.member_id = cv_member_id; -- Define a dynamic cursor to find a key for an individual or group member type. CURSOR get_lookup_id (cv_type VARCHAR2) IS SELECT common_lookup_id FROM common_lookup WHERE common_lookup_context = 'MEMBER' AND common_lookup_type = cv_type; BEGIN -- Get the number of foreign key values for a contact. OPEN count_contact(pv_member_id); FETCH count_contact INTO lv_dependent; CLOSE count_contact; -- Open the dynamic cursor with the required value. IF lv_dependent = 1 THEN OPEN get_lookup_id(lv_member_type(1)); ELSE OPEN get_lookup_id(lv_member_type(2)); END IF; -- Get the correct surrogate primary key value. FETCH get_lookup_id INTO lv_return_id; CLOSE get_lookup_id; -- Return the correct primary key for use as a foreign key. RETURN lv_return_id; END; / |
MySQL User-Defined Function (UDF) ↓
Expand this section to see how to map this logic to a Persistent Stored Module (PSM) function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | SELECT 'DROP FUNCTION IF EXISTS get_member_type' AS "Statement"; DROP FUNCTION IF EXISTS get_member_type; SELECT 'DELIMITER $$' AS "Statement"; DELIMITER $$ SELECT 'CREATE FUNCTION get_member_type' AS "Statement"$$ CREATE FUNCTION get_member_type(pv_member_id INT) RETURNS INT BEGIN /* Define three local variables. */ DECLARE lv_contact_number INT; DECLARE lv_member_type CHAR(30); DECLARE lv_return_value INT; /* Define a dynamic cursor to count the number of foreign key values. */ DECLARE contact_cursor CURSOR FOR SELECT COUNT(c.member_id) FROM contact c WHERE c.member_id = pv_member_id; /* Define a dynamic cursor to find a key for an individual or group member type. */ DECLARE common_lookup_cursor CURSOR FOR SELECT common_lookup_id FROM common_lookup WHERE common_lookup_table = 'MEMBER' AND common_lookup_column = 'MEMBER_TYPE' AND common_lookup_type = lv_member_type; /* Get the number of foreign key values for a contact. */ OPEN contact_cursor; FETCH contact_cursor INTO lv_contact_number; CLOSE contact_cursor; /* Assign group membership when more than one foreign key value is found; and assign individual membership when only one foreign key value is found. */ IF lv_contact_number > 1 THEN SET lv_member_type = 'GROUP'; ELSE SET lv_member_type = 'INDIVIDUAL'; END IF; /* Get the correct surrogate primary key value. */ OPEN common_lookup_cursor; FETCH common_lookup_cursor INTO lv_return_value; CLOSE common_lookup_cursor; /* Return the correct primary key for use as a foreign key. */ RETURN lv_return_value; END; $$ SELECT 'DELIMITER $$' AS "Statement"$$ DELIMITER ; |
You can query the results of the update statement with the following.
Change Confirmation Query ↓
Expand this section to see the query that lets you examine the changes. It runs in either Oracle or MySQL without any changes.
1 2 3 4 5 6 7 8 9 10 11 | SELECT m.member_id , dt.quantity , m.member_type , cl.common_lookup_type FROM member m JOIN (SELECT member_id , COUNT(c.member_id) AS quantity FROM contact c GROUP BY c.member_id) dt ON m.member_id = dt.member_id JOIN common_lookup cl ON m.member_type = cl.common_lookup_id ORDER BY m.member_id; |
As always, I look forward to helping and gaining insight.
Debugging MySQL Functions
Somebody, who read this post on Debugging MySQL Procedures, asked why the strategy of selecting a string literal didn’t work in a MySQL function. That’s easy, they’re not designed to support a SELECT
statement, only a SELECT-INTO
statement.
Why? That’s the purpose of a function to perform something and return a single reply.
That’s also why a MySQL functions only support the IN
mode of operation for formal and call parameters. When formal parameters are restricted to in-mode-only operations, they implement a pass-by-value function model. This can also be expressed from the other side of the looking glass. In that case, MySQL functions don’t support pass-by-reference functions that use the INOUT
or OUT
mode operations.
If you put a SELECT
statement in a function to print internal values or comments, it raises an error. Take for example the following attempt to create the debugging
function with an echo of output (that works in stored procedures).
CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT '[Debug #1]'; RETURN 1; END; $$ |
It fails to create the function because you’ve violated a key integrity rule. It also raises the following error:
ERROR 1415 (0A000): Not allowed to return a result set from a function |
You have two potential solutions to this problem. The first is limited and inflexible. The second isn’t as limited or inflexible and is the recommended way to debug your functions without a tool. That’s to use a temporary table to record run-time debugging events.
Session Variable Debugging ↓
Expand this section to see the steps for debugging functions with session variables.
- Create two session level variables, like these:
SET @counter := 0; SET @msg := ''; |
- Create a function that uses the
SELECT-INTO
statement to collect and store debugging information during function execution.
CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT @counter + 1 INTO @counter; SELECT CONCAT('[Debug #',@counter,']') INTO @msg; RETURN 1; END; $$ |
- Run the function and then query the session variable for results
SELECT debugger(); SELECT @msg; |
You’ll see the following text:
+------------+ | @msg | +------------+ | [Debug #1] | +------------+ |
Temporary Table Debugging ↓
Expand this section to see the steps for debugging functions with session variables.
- Only when you want a counter, create one session level variables.
SET @counter := 0; |
- Create an in-memory table to store debugging information from function execution.
CREATE TABLE debugger ( debug_comment CHAR(80)) ENGINE=MEMORY; |
- Create a function that supports inserts into the in-memory table. Naturally, you may need to make the columns larger when your debugging results are large. I’ve found that 80 characters is generally adequate for most debugging exercises.
1 2 3 4 5 6 7 | CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT @counter + 1 INTO @counter; INSERT INTO debugger VALUES (CONCAT('[Debug #',@counter,']')); RETURN 1; END; $$ |
- Call the function and query the debugging results.
SELECT debugger(); SELECT debugger(); SELECT debugger(); SELECT debug_comment FROM debugger; |
You’ll see the following text:
+---------------+ | debug_comment | +---------------+ | [Debug #1] | | [Debug #2] | | [Debug #3] | +---------------+ |
Complete Code Sample ↓
Expand this section to see the sample working code for all examples.
This script creates, runs, and tests the code from the above discussions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | -- Conditionally drop the function when it exists. DROP FUNCTION IF EXISTS debugger; -- Set delimiter to create a function with semicolon statment terminators. DELIMITER $$ -- Create a function that returns 1. CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT '[Debug #1]'; RETURN 1; END; $$ -- Reset the delimiter to enable normal execution. DELIMITER ; -- Declare session level variables. SET @counter := 0; SET @msg := ''; -- Conditionally drop the function when it exists. DROP FUNCTION IF EXISTS debugger; -- Set delimiter to create a function with semicolon statment terminators. DELIMITER $$ -- Create a function that writes to local session variables. CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT @counter + 1 INTO @counter; SELECT CONCAT('[Debug #',@counter,']') INTO @msg; RETURN 1; END; $$ -- Reset the delimiter to enable normal execution. DELIMITER ; -- Test the function code and read the session-level variable contents. SELECT debugger(); SELECT @msg; -- Declare a session level variable. SET @counter := 0; -- Conditionally drop the function when it exists. DROP TABLE IF EXISTS debugger; -- Create a temporary (in-memory) table to record debugging information. CREATE TABLE debugger ( debug_comment CHAR(80)) ENGINE=MEMORY; -- Conditionally drop the function when it exists. DROP FUNCTION IF EXISTS debugger; -- Set delimiter to create a function with semicolon statment terminators. DELIMITER $$ -- Create a function that writes to a debugging table. CREATE FUNCTION debugger() RETURNS INT BEGIN SELECT @counter + 1 INTO @counter; INSERT INTO debugger VALUES (CONCAT('[Debug #',@counter,']')); RETURN 1; END; $$ -- Reset the delimiter to enable normal execution. DELIMITER ; -- Test the function code and read the session-level variable contents. SELECT debugger(); SELECT debugger(); SELECT debugger(); SELECT debug_comment FROM debugger; |