MDL-69542 enrol_lti: add published resource view object support
[moodle.git] / lib / adodb / adodb-xmlschema03.inc.php
blobde1ea26c7d6727298df1a58aa28bb33cf15b9b55
1 <?php
2 /**
3 * ADOdb XML Schema (v0.3).
5 * xmlschema is a class that allows the user to quickly and easily
6 * build a database on any ADOdb-supported platform using a simple
7 * XML schema.
9 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
11 * @package ADOdb
12 * @link https://adodb.org Project's web site and documentation
13 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker
15 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause
16 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option,
17 * any later version. This means you can use it in proprietary products.
18 * See the LICENSE.md file distributed with this source code for details.
19 * @license BSD-3-Clause
20 * @license LGPL-2.1-or-later
22 * @copyright 2004-2005 ars Cognita Inc., all rights reserved
23 * @copyright 2005-2013 John Lim
24 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
25 * @author Richard Tango-Lowy
26 * @author Dan Cech
29 function _file_get_contents($file)
31 if (function_exists('file_get_contents')) return file_get_contents($file);
33 $f = fopen($file,'r');
34 if (!$f) return '';
35 $t = '';
37 while ($s = fread($f,100000)) $t .= $s;
38 fclose($f);
39 return $t;
43 /**
44 * Debug on or off
46 if( !defined( 'XMLS_DEBUG' ) ) {
47 define( 'XMLS_DEBUG', FALSE );
50 /**
51 * Default prefix key
53 if( !defined( 'XMLS_PREFIX' ) ) {
54 define( 'XMLS_PREFIX', '%%P' );
57 /**
58 * Maximum length allowed for object prefix
60 if( !defined( 'XMLS_PREFIX_MAXLEN' ) ) {
61 define( 'XMLS_PREFIX_MAXLEN', 10 );
64 /**
65 * Execute SQL inline as it is generated
67 if( !defined( 'XMLS_EXECUTE_INLINE' ) ) {
68 define( 'XMLS_EXECUTE_INLINE', FALSE );
71 /**
72 * Continue SQL Execution if an error occurs?
74 if( !defined( 'XMLS_CONTINUE_ON_ERROR' ) ) {
75 define( 'XMLS_CONTINUE_ON_ERROR', FALSE );
78 /**
79 * Current Schema Version
81 if( !defined( 'XMLS_SCHEMA_VERSION' ) ) {
82 define( 'XMLS_SCHEMA_VERSION', '0.3' );
85 /**
86 * Default Schema Version. Used for Schemas without an explicit version set.
88 if( !defined( 'XMLS_DEFAULT_SCHEMA_VERSION' ) ) {
89 define( 'XMLS_DEFAULT_SCHEMA_VERSION', '0.1' );
92 /**
93 * How to handle data rows that already exist in a database during and upgrade.
94 * Options are INSERT (attempts to insert duplicate rows), UPDATE (updates existing
95 * rows) and IGNORE (ignores existing rows).
97 if( !defined( 'XMLS_MODE_INSERT' ) ) {
98 define( 'XMLS_MODE_INSERT', 0 );
100 if( !defined( 'XMLS_MODE_UPDATE' ) ) {
101 define( 'XMLS_MODE_UPDATE', 1 );
103 if( !defined( 'XMLS_MODE_IGNORE' ) ) {
104 define( 'XMLS_MODE_IGNORE', 2 );
106 if( !defined( 'XMLS_EXISTING_DATA' ) ) {
107 define( 'XMLS_EXISTING_DATA', XMLS_MODE_INSERT );
111 * Default Schema Version. Used for Schemas without an explicit version set.
113 if( !defined( 'XMLS_DEFAULT_UPGRADE_METHOD' ) ) {
114 define( 'XMLS_DEFAULT_UPGRADE_METHOD', 'ALTER' );
118 * Include the main ADODB library
120 if( !defined( '_ADODB_LAYER' ) ) {
121 require( 'adodb.inc.php' );
122 require( 'adodb-datadict.inc.php' );
126 * Abstract DB Object. This class provides basic methods for database objects, such
127 * as tables and indexes.
129 * @package axmls
130 * @access private
132 class dbObject {
135 * var object Parent
137 var $parent;
140 * var string current element
142 var $currentElement;
145 * NOP
147 function __construct( &$parent, $attributes = NULL ) {
148 $this->parent = $parent;
152 * XML Callback to process start elements
154 * @access private
156 function _tag_open( $parser, $tag, $attributes ) {
161 * XML Callback to process CDATA elements
163 * @access private
165 function _tag_cdata( $parser, $cdata ) {
170 * XML Callback to process end elements
172 * @access private
174 function _tag_close( $parser, $tag ) {
178 function create(&$xmls) {
179 return array();
183 * Destroys the object
185 function destroy() {
189 * Checks whether the specified RDBMS is supported by the current
190 * database object or its ranking ancestor.
192 * @param string $platform RDBMS platform name (from ADODB platform list).
193 * @return boolean TRUE if RDBMS is supported; otherwise returns FALSE.
195 function supportedPlatform( $platform = NULL ) {
196 return is_object( $this->parent ) ? $this->parent->supportedPlatform( $platform ) : TRUE;
200 * Returns the prefix set by the ranking ancestor of the database object.
202 * @param string $name Prefix string.
203 * @return string Prefix.
205 function prefix( $name = '' ) {
206 return is_object( $this->parent ) ? $this->parent->prefix( $name ) : $name;
210 * Extracts a field ID from the specified field.
212 * @param string $field Field.
213 * @return string Field ID.
215 function fieldID( $field ) {
216 return strtoupper( preg_replace( '/^`(.+)`$/', '$1', $field ) );
221 * Creates a table object in ADOdb's datadict format
223 * This class stores information about a database table. As charactaristics
224 * of the table are loaded from the external source, methods and properties
225 * of this class are used to build up the table description in ADOdb's
226 * datadict format.
228 * @package axmls
229 * @access private
231 class dbTable extends dbObject {
234 * @var string Table name
236 var $name;
239 * @var array Field specifier: Meta-information about each field
241 var $fields = array();
244 * @var array List of table indexes.
246 var $indexes = array();
249 * @var array Table options: Table-level options
251 var $opts = array();
254 * @var string Field index: Keeps track of which field is currently being processed
256 var $current_field;
259 * @var boolean Mark table for destruction
260 * @access private
262 var $drop_table;
265 * @var boolean Mark field for destruction (not yet implemented)
266 * @access private
268 var $drop_field = array();
271 * @var array Platform-specific options
272 * @access private
274 var $currentPlatform = true;
278 * Iniitializes a new table object.
280 * @param string $prefix DB Object prefix
281 * @param array $attributes Array of table attributes.
283 function __construct( &$parent, $attributes = NULL ) {
284 $this->parent = $parent;
285 $this->name = $this->prefix($attributes['NAME']);
289 * XML Callback to process start elements. Elements currently
290 * processed are: INDEX, DROP, FIELD, KEY, NOTNULL, AUTOINCREMENT & DEFAULT.
292 * @access private
294 function _tag_open( $parser, $tag, $attributes ) {
295 $this->currentElement = strtoupper( $tag );
297 switch( $this->currentElement ) {
298 case 'INDEX':
299 if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
300 $index = $this->addIndex( $attributes );
301 xml_set_object( $parser, $index );
303 break;
304 case 'DATA':
305 if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
306 $data = $this->addData( $attributes );
307 xml_set_object( $parser, $data );
309 break;
310 case 'DROP':
311 $this->drop();
312 break;
313 case 'FIELD':
314 // Add a field
315 $fieldName = $attributes['NAME'];
316 $fieldType = $attributes['TYPE'];
317 $fieldSize = isset( $attributes['SIZE'] ) ? $attributes['SIZE'] : NULL;
318 $fieldOpts = !empty( $attributes['OPTS'] ) ? $attributes['OPTS'] : NULL;
320 $this->addField( $fieldName, $fieldType, $fieldSize, $fieldOpts );
321 break;
322 case 'KEY':
323 case 'NOTNULL':
324 case 'AUTOINCREMENT':
325 case 'DEFDATE':
326 case 'DEFTIMESTAMP':
327 case 'UNSIGNED':
328 // Add a field option
329 $this->addFieldOpt( $this->current_field, $this->currentElement );
330 break;
331 case 'DEFAULT':
332 // Add a field option to the table object
334 // Work around ADOdb datadict issue that misinterprets empty strings.
335 if( $attributes['VALUE'] == '' ) {
336 $attributes['VALUE'] = " '' ";
339 $this->addFieldOpt( $this->current_field, $this->currentElement, $attributes['VALUE'] );
340 break;
341 case 'OPT':
342 case 'CONSTRAINT':
343 // Accept platform-specific options
344 $this->currentPlatform = ( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) );
345 break;
346 default:
347 // print_r( array( $tag, $attributes ) );
352 * XML Callback to process CDATA elements
354 * @access private
356 function _tag_cdata( $parser, $cdata ) {
357 switch( $this->currentElement ) {
358 // Table or field comment
359 case 'DESCR':
360 if( isset( $this->current_field ) ) {
361 $this->addFieldOpt( $this->current_field, $this->currentElement, $cdata );
362 } else {
363 $this->addTableComment( $cdata );
365 break;
366 // Table/field constraint
367 case 'CONSTRAINT':
368 if( isset( $this->current_field ) ) {
369 $this->addFieldOpt( $this->current_field, $this->currentElement, $cdata );
370 } else {
371 $this->addTableOpt( $cdata );
373 break;
374 // Table/field option
375 case 'OPT':
376 if( isset( $this->current_field ) ) {
377 $this->addFieldOpt( $this->current_field, $cdata );
378 } else {
379 $this->addTableOpt( $cdata );
381 break;
382 default:
388 * XML Callback to process end elements
390 * @access private
392 function _tag_close( $parser, $tag ) {
393 $this->currentElement = '';
395 switch( strtoupper( $tag ) ) {
396 case 'TABLE':
397 $this->parent->addSQL( $this->create( $this->parent ) );
398 xml_set_object( $parser, $this->parent );
399 $this->destroy();
400 break;
401 case 'FIELD':
402 unset($this->current_field);
403 break;
404 case 'OPT':
405 case 'CONSTRAINT':
406 $this->currentPlatform = true;
407 break;
408 default:
414 * Adds an index to a table object
416 * @param array $attributes Index attributes
417 * @return object dbIndex object
419 function addIndex( $attributes ) {
420 $name = strtoupper( $attributes['NAME'] );
421 $this->indexes[$name] = new dbIndex( $this, $attributes );
422 return $this->indexes[$name];
426 * Adds data to a table object
428 * @param array $attributes Data attributes
429 * @return object dbData object
431 function addData( $attributes ) {
432 if( !isset( $this->data ) ) {
433 $this->data = new dbData( $this, $attributes );
435 return $this->data;
439 * Adds a field to a table object
441 * $name is the name of the table to which the field should be added.
442 * $type is an ADODB datadict field type. The following field types
443 * are supported as of ADODB 3.40:
444 * - C: varchar
445 * - X: CLOB (character large object) or largest varchar size
446 * if CLOB is not supported
447 * - C2: Multibyte varchar
448 * - X2: Multibyte CLOB
449 * - B: BLOB (binary large object)
450 * - D: Date (some databases do not support this, and we return a datetime type)
451 * - T: Datetime or Timestamp
452 * - L: Integer field suitable for storing booleans (0 or 1)
453 * - I: Integer (mapped to I4)
454 * - I1: 1-byte integer
455 * - I2: 2-byte integer
456 * - I4: 4-byte integer
457 * - I8: 8-byte integer
458 * - F: Floating point number
459 * - N: Numeric or decimal number
461 * @param string $name Name of the table to which the field will be added.
462 * @param string $type ADODB datadict field type.
463 * @param string $size Field size
464 * @param array $opts Field options array
465 * @return array Field specifier array
467 function addField( $name, $type, $size = NULL, $opts = NULL ) {
468 $field_id = $this->fieldID( $name );
470 // Set the field index so we know where we are
471 $this->current_field = $field_id;
473 // Set the field name (required)
474 $this->fields[$field_id]['NAME'] = $name;
476 // Set the field type (required)
477 $this->fields[$field_id]['TYPE'] = $type;
479 // Set the field size (optional)
480 if( isset( $size ) ) {
481 $this->fields[$field_id]['SIZE'] = $size;
484 // Set the field options
485 if( isset( $opts ) ) {
486 $this->fields[$field_id]['OPTS'] = array($opts);
487 } else {
488 $this->fields[$field_id]['OPTS'] = array();
493 * Adds a field option to the current field specifier
495 * This method adds a field option allowed by the ADOdb datadict
496 * and appends it to the given field.
498 * @param string $field Field name
499 * @param string $opt ADOdb field option
500 * @param mixed $value Field option value
501 * @return array Field specifier array
503 function addFieldOpt( $field, $opt, $value = NULL ) {
504 if( $this->currentPlatform ) {
505 if( !isset( $value ) ) {
506 $this->fields[$this->FieldID( $field )]['OPTS'][] = $opt;
507 // Add the option and value
508 } else {
509 $this->fields[$this->FieldID( $field )]['OPTS'][] = array( $opt => $value );
515 * Adds an option to the table
517 * This method takes a comma-separated list of table-level options
518 * and appends them to the table object.
520 * @param string $opt Table option
521 * @return array Options
523 function addTableOpt( $opt ) {
524 if(isset($this->currentPlatform)) {
525 $this->opts[$this->parent->db->dataProvider] = $opt;
527 return $this->opts;
530 function addTableComment( $opt ) {
531 $this->opts['comment'] = $opt;
532 return $this->opts;
536 * Generates the SQL that will create the table in the database
538 * @param object $xmls adoSchema object
539 * @return array Array containing table creation SQL
541 function create( &$xmls ) {
542 $sql = array();
544 // drop any existing indexes
545 if( is_array( $legacy_indexes = $xmls->dict->metaIndexes( $this->name ) ) ) {
546 foreach( $legacy_indexes as $index => $index_details ) {
547 $sql[] = $xmls->dict->dropIndexSQL( $index, $this->name );
551 // remove fields to be dropped from table object
552 foreach( $this->drop_field as $field ) {
553 unset( $this->fields[$field] );
556 // if table exists
557 if( is_array( $legacy_fields = $xmls->dict->metaColumns( $this->name ) ) ) {
558 // drop table
559 if( $this->drop_table ) {
560 $sql[] = $xmls->dict->dropTableSQL( $this->name );
562 return $sql;
565 // drop any existing fields not in schema
566 foreach( $legacy_fields as $field_id => $field ) {
567 if( !isset( $this->fields[$field_id] ) ) {
568 $sql[] = $xmls->dict->dropColumnSQL( $this->name, $field->name );
571 // if table doesn't exist
572 } else {
573 if( $this->drop_table ) {
574 return $sql;
577 $legacy_fields = array();
580 // Loop through the field specifier array, building the associative array for the field options
581 $fldarray = array();
583 foreach( $this->fields as $field_id => $finfo ) {
584 // Set an empty size if it isn't supplied
585 if( !isset( $finfo['SIZE'] ) ) {
586 $finfo['SIZE'] = '';
589 // Initialize the field array with the type and size
590 $fldarray[$field_id] = array(
591 'NAME' => $finfo['NAME'],
592 'TYPE' => $finfo['TYPE'],
593 'SIZE' => $finfo['SIZE']
596 // Loop through the options array and add the field options.
597 if( isset( $finfo['OPTS'] ) ) {
598 foreach( $finfo['OPTS'] as $opt ) {
599 // Option has an argument.
600 if( is_array( $opt ) ) {
601 $key = key( $opt );
602 $value = $opt[key( $opt )];
603 @$fldarray[$field_id][$key] .= $value;
604 // Option doesn't have arguments
605 } else {
606 $fldarray[$field_id][$opt] = $opt;
612 if( empty( $legacy_fields ) ) {
613 // Create the new table
614 $sql[] = $xmls->dict->createTableSQL( $this->name, $fldarray, $this->opts );
615 logMsg( end( $sql ), 'Generated createTableSQL' );
616 } else {
617 // Upgrade an existing table
618 logMsg( "Upgrading {$this->name} using '{$xmls->upgrade}'" );
619 switch( $xmls->upgrade ) {
620 // Use ChangeTableSQL
621 case 'ALTER':
622 logMsg( 'Generated changeTableSQL (ALTERing table)' );
623 $sql[] = $xmls->dict->changeTableSQL( $this->name, $fldarray, $this->opts );
624 break;
625 case 'REPLACE':
626 logMsg( 'Doing upgrade REPLACE (testing)' );
627 $sql[] = $xmls->dict->dropTableSQL( $this->name );
628 $sql[] = $xmls->dict->createTableSQL( $this->name, $fldarray, $this->opts );
629 break;
630 // ignore table
631 default:
632 return array();
636 foreach( $this->indexes as $index ) {
637 $sql[] = $index->create( $xmls );
640 if( isset( $this->data ) ) {
641 $sql[] = $this->data->create( $xmls );
644 return $sql;
648 * Marks a field or table for destruction
650 function drop() {
651 if( isset( $this->current_field ) ) {
652 // Drop the current field
653 logMsg( "Dropping field '{$this->current_field}' from table '{$this->name}'" );
654 // $this->drop_field[$this->current_field] = $xmls->dict->DropColumnSQL( $this->name, $this->current_field );
655 $this->drop_field[$this->current_field] = $this->current_field;
656 } else {
657 // Drop the current table
658 logMsg( "Dropping table '{$this->name}'" );
659 // $this->drop_table = $xmls->dict->DropTableSQL( $this->name );
660 $this->drop_table = TRUE;
666 * Creates an index object in ADOdb's datadict format
668 * This class stores information about a database index. As charactaristics
669 * of the index are loaded from the external source, methods and properties
670 * of this class are used to build up the index description in ADOdb's
671 * datadict format.
673 * @package axmls
674 * @access private
676 class dbIndex extends dbObject {
679 * @var string Index name
681 var $name;
684 * @var array Index options: Index-level options
686 var $opts = array();
689 * @var array Indexed fields: Table columns included in this index
691 var $columns = array();
694 * @var boolean Mark index for destruction
695 * @access private
697 var $drop = FALSE;
700 * Initializes the new dbIndex object.
702 * @param object $parent Parent object
703 * @param array $attributes Attributes
705 * @internal
707 function __construct( &$parent, $attributes = NULL ) {
708 $this->parent = $parent;
710 $this->name = $this->prefix ($attributes['NAME']);
714 * XML Callback to process start elements
716 * Processes XML opening tags.
717 * Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH.
719 * @access private
721 function _tag_open( $parser, $tag, $attributes ) {
722 $this->currentElement = strtoupper( $tag );
724 switch( $this->currentElement ) {
725 case 'DROP':
726 $this->drop();
727 break;
728 case 'CLUSTERED':
729 case 'BITMAP':
730 case 'UNIQUE':
731 case 'FULLTEXT':
732 case 'HASH':
733 // Add index Option
734 $this->addIndexOpt( $this->currentElement );
735 break;
736 default:
737 // print_r( array( $tag, $attributes ) );
742 * XML Callback to process CDATA elements
744 * Processes XML cdata.
746 * @access private
748 function _tag_cdata( $parser, $cdata ) {
749 switch( $this->currentElement ) {
750 // Index field name
751 case 'COL':
752 $this->addField( $cdata );
753 break;
754 default:
760 * XML Callback to process end elements
762 * @access private
764 function _tag_close( $parser, $tag ) {
765 $this->currentElement = '';
767 switch( strtoupper( $tag ) ) {
768 case 'INDEX':
769 xml_set_object( $parser, $this->parent );
770 break;
775 * Adds a field to the index
777 * @param string $name Field name
778 * @return string Field list
780 function addField( $name ) {
781 $this->columns[$this->fieldID( $name )] = $name;
783 // Return the field list
784 return $this->columns;
788 * Adds options to the index
790 * @param string $opt Comma-separated list of index options.
791 * @return string Option list
793 function addIndexOpt( $opt ) {
794 $this->opts[] = $opt;
796 // Return the options list
797 return $this->opts;
801 * Generates the SQL that will create the index in the database
803 * @param object $xmls adoSchema object
804 * @return array Array containing index creation SQL
806 function create( &$xmls ) {
807 if( $this->drop ) {
808 return NULL;
811 // eliminate any columns that aren't in the table
812 foreach( $this->columns as $id => $col ) {
813 if( !isset( $this->parent->fields[$id] ) ) {
814 unset( $this->columns[$id] );
818 return $xmls->dict->createIndexSQL( $this->name, $this->parent->name, $this->columns, $this->opts );
822 * Marks an index for destruction
824 function drop() {
825 $this->drop = TRUE;
830 * Creates a data object in ADOdb's datadict format
832 * This class stores information about table data, and is called
833 * when we need to load field data into a table.
835 * @package axmls
836 * @access private
838 class dbData extends dbObject {
840 var $data = array();
842 var $row;
845 * Initializes the new dbData object.
847 * @param object $parent Parent object
848 * @param array $attributes Attributes
850 * @internal
852 function __construct( &$parent, $attributes = NULL ) {
853 $this->parent = $parent;
857 * XML Callback to process start elements
859 * Processes XML opening tags.
860 * Elements currently processed are: ROW and F (field).
862 * @access private
864 function _tag_open( $parser, $tag, $attributes ) {
865 $this->currentElement = strtoupper( $tag );
867 switch( $this->currentElement ) {
868 case 'ROW':
869 $this->row = count( $this->data );
870 $this->data[$this->row] = array();
871 break;
872 case 'F':
873 $this->addField($attributes);
874 default:
875 // print_r( array( $tag, $attributes ) );
880 * XML Callback to process CDATA elements
882 * Processes XML cdata.
884 * @access private
886 function _tag_cdata( $parser, $cdata ) {
887 switch( $this->currentElement ) {
888 // Index field name
889 case 'F':
890 $this->addData( $cdata );
891 break;
892 default:
898 * XML Callback to process end elements
900 * @access private
902 function _tag_close( $parser, $tag ) {
903 $this->currentElement = '';
905 switch( strtoupper( $tag ) ) {
906 case 'DATA':
907 xml_set_object( $parser, $this->parent );
908 break;
913 * Adds a field to the insert
915 * @param string $name Field name
916 * @return string Field list
918 function addField( $attributes ) {
919 // check we're in a valid row
920 if( !isset( $this->row ) || !isset( $this->data[$this->row] ) ) {
921 return;
924 // Set the field index so we know where we are
925 if( isset( $attributes['NAME'] ) ) {
926 $this->current_field = $this->fieldID( $attributes['NAME'] );
927 } else {
928 $this->current_field = count( $this->data[$this->row] );
931 // initialise data
932 if( !isset( $this->data[$this->row][$this->current_field] ) ) {
933 $this->data[$this->row][$this->current_field] = '';
938 * Adds options to the index
940 * @param string $opt Comma-separated list of index options.
941 * @return string Option list
943 function addData( $cdata ) {
944 // check we're in a valid field
945 if ( isset( $this->data[$this->row][$this->current_field] ) ) {
946 // add data to field
947 $this->data[$this->row][$this->current_field] .= $cdata;
952 * Generates the SQL that will add/update the data in the database
954 * @param object $xmls adoSchema object
955 * @return array Array containing index creation SQL
957 function create( &$xmls ) {
958 $table = $xmls->dict->tableName($this->parent->name);
959 $table_field_count = count($this->parent->fields);
960 $tables = $xmls->db->metaTables();
961 $sql = array();
963 $ukeys = $xmls->db->metaPrimaryKeys( $table );
964 if( !empty( $this->parent->indexes ) and !empty( $ukeys ) ) {
965 foreach( $this->parent->indexes as $indexObj ) {
966 if( !in_array( $indexObj->name, $ukeys ) ) $ukeys[] = $indexObj->name;
970 // eliminate any columns that aren't in the table
971 foreach( $this->data as $row ) {
972 $table_fields = $this->parent->fields;
973 $fields = array();
974 $rawfields = array(); // Need to keep some of the unprocessed data on hand.
976 foreach( $row as $field_id => $field_data ) {
977 if( !array_key_exists( $field_id, $table_fields ) ) {
978 if( is_numeric( $field_id ) ) {
979 $field_id = reset( array_keys( $table_fields ) );
980 } else {
981 continue;
985 $name = $table_fields[$field_id]['NAME'];
987 switch( $table_fields[$field_id]['TYPE'] ) {
988 case 'I':
989 case 'I1':
990 case 'I2':
991 case 'I4':
992 case 'I8':
993 $fields[$name] = intval($field_data);
994 break;
995 case 'C':
996 case 'C2':
997 case 'X':
998 case 'X2':
999 default:
1000 $fields[$name] = $xmls->db->qstr( $field_data );
1001 $rawfields[$name] = $field_data;
1004 unset($table_fields[$field_id]);
1008 // check that at least 1 column is specified
1009 if( empty( $fields ) ) {
1010 continue;
1013 // check that no required columns are missing
1014 if( count( $fields ) < $table_field_count ) {
1015 foreach( $table_fields as $field ) {
1016 if( isset( $field['OPTS'] ) and ( in_array( 'NOTNULL', $field['OPTS'] ) || in_array( 'KEY', $field['OPTS'] ) ) && !in_array( 'AUTOINCREMENT', $field['OPTS'] ) ) {
1017 continue(2);
1022 // The rest of this method deals with updating existing data records.
1024 if( !in_array( $table, $tables ) or ( $mode = $xmls->existingData() ) == XMLS_MODE_INSERT ) {
1025 // Table doesn't yet exist, so it's safe to insert.
1026 logMsg( "$table doesn't exist, inserting or mode is INSERT" );
1027 $sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')';
1028 continue;
1031 // Prepare to test for potential violations. Get primary keys and unique indexes
1032 $mfields = array_merge( $fields, $rawfields );
1033 $keyFields = array_intersect( $ukeys, array_keys( $mfields ) );
1035 if( empty( $ukeys ) or count( $keyFields ) == 0 ) {
1036 // No unique keys in schema, so safe to insert
1037 logMsg( "Either schema or data has no unique keys, so safe to insert" );
1038 $sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')';
1039 continue;
1042 // Select record containing matching unique keys.
1043 $where = '';
1044 foreach( $ukeys as $key ) {
1045 if( isset( $mfields[$key] ) and $mfields[$key] ) {
1046 if( $where ) $where .= ' AND ';
1047 $where .= $key . ' = ' . $xmls->db->qstr( $mfields[$key] );
1050 $records = $xmls->db->execute( 'SELECT * FROM ' . $table . ' WHERE ' . $where );
1051 switch( $records->recordCount() ) {
1052 case 0:
1053 // No matching record, so safe to insert.
1054 logMsg( "No matching records. Inserting new row with unique data" );
1055 $sql[] = $xmls->db->getInsertSQL( $records, $mfields );
1056 break;
1057 case 1:
1058 // Exactly one matching record, so we can update if the mode permits.
1059 logMsg( "One matching record..." );
1060 if( $mode == XMLS_MODE_UPDATE ) {
1061 logMsg( "...Updating existing row from unique data" );
1062 $sql[] = $xmls->db->getUpdateSQL( $records, $mfields );
1064 break;
1065 default:
1066 // More than one matching record; the result is ambiguous, so we must ignore the row.
1067 logMsg( "More than one matching record. Ignoring row." );
1070 return $sql;
1075 * Creates the SQL to execute a list of provided SQL queries
1077 * @package axmls
1078 * @access private
1080 class dbQuerySet extends dbObject {
1083 * @var array List of SQL queries
1085 var $queries = array();
1088 * @var string String used to build of a query line by line
1090 var $query;
1093 * @var string Query prefix key
1095 var $prefixKey = '';
1098 * @var boolean Auto prefix enable (TRUE)
1100 var $prefixMethod = 'AUTO';
1103 * Initializes the query set.
1105 * @param object $parent Parent object
1106 * @param array $attributes Attributes
1108 function __construct( &$parent, $attributes = NULL ) {
1109 $this->parent = $parent;
1111 // Overrides the manual prefix key
1112 if( isset( $attributes['KEY'] ) ) {
1113 $this->prefixKey = $attributes['KEY'];
1116 $prefixMethod = isset( $attributes['PREFIXMETHOD'] ) ? strtoupper( trim( $attributes['PREFIXMETHOD'] ) ) : '';
1118 // Enables or disables automatic prefix prepending
1119 switch( $prefixMethod ) {
1120 case 'AUTO':
1121 $this->prefixMethod = 'AUTO';
1122 break;
1123 case 'MANUAL':
1124 $this->prefixMethod = 'MANUAL';
1125 break;
1126 case 'NONE':
1127 $this->prefixMethod = 'NONE';
1128 break;
1133 * XML Callback to process start elements. Elements currently
1134 * processed are: QUERY.
1136 * @access private
1138 function _tag_open( $parser, $tag, $attributes ) {
1139 $this->currentElement = strtoupper( $tag );
1141 switch( $this->currentElement ) {
1142 case 'QUERY':
1143 // Create a new query in a SQL queryset.
1144 // Ignore this query set if a platform is specified and it's different than the
1145 // current connection platform.
1146 if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1147 $this->newQuery();
1148 } else {
1149 $this->discardQuery();
1151 break;
1152 default:
1153 // print_r( array( $tag, $attributes ) );
1158 * XML Callback to process CDATA elements
1160 function _tag_cdata( $parser, $cdata ) {
1161 switch( $this->currentElement ) {
1162 // Line of queryset SQL data
1163 case 'QUERY':
1164 $this->buildQuery( $cdata );
1165 break;
1166 default:
1172 * XML Callback to process end elements
1174 * @access private
1176 function _tag_close( $parser, $tag ) {
1177 $this->currentElement = '';
1179 switch( strtoupper( $tag ) ) {
1180 case 'QUERY':
1181 // Add the finished query to the open query set.
1182 $this->addQuery();
1183 break;
1184 case 'SQL':
1185 $this->parent->addSQL( $this->create( $this->parent ) );
1186 xml_set_object( $parser, $this->parent );
1187 $this->destroy();
1188 break;
1189 default:
1195 * Re-initializes the query.
1197 * @return boolean TRUE
1199 function newQuery() {
1200 $this->query = '';
1202 return TRUE;
1206 * Discards the existing query.
1208 * @return boolean TRUE
1210 function discardQuery() {
1211 unset( $this->query );
1213 return TRUE;
1217 * Appends a line to a query that is being built line by line
1219 * @param string $data Line of SQL data or NULL to initialize a new query
1220 * @return string SQL query string.
1222 function buildQuery( $sql = NULL ) {
1223 if( !isset( $this->query ) OR empty( $sql ) ) {
1224 return FALSE;
1227 $this->query .= $sql;
1229 return $this->query;
1233 * Adds a completed query to the query list
1235 * @return string SQL of added query
1237 function addQuery() {
1238 if( !isset( $this->query ) ) {
1239 return FALSE;
1242 $this->queries[] = $return = trim($this->query);
1244 unset( $this->query );
1246 return $return;
1250 * Creates and returns the current query set
1252 * @param object $xmls adoSchema object
1253 * @return array Query set
1255 function create( &$xmls ) {
1256 foreach( $this->queries as $id => $query ) {
1257 switch( $this->prefixMethod ) {
1258 case 'AUTO':
1259 // Enable auto prefix replacement
1261 // Process object prefix.
1262 // Evaluate SQL statements to prepend prefix to objects
1263 $query = $this->prefixQuery( '/^\s*((?is)INSERT\s+(INTO\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1264 $query = $this->prefixQuery( '/^\s*((?is)UPDATE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1265 $query = $this->prefixQuery( '/^\s*((?is)DELETE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1267 // SELECT statements aren't working yet
1268 #$data = preg_replace( '/(?ias)(^\s*SELECT\s+.*\s+FROM)\s+(\W\s*,?\s*)+((?i)\s+WHERE.*$)/', "\1 $prefix\2 \3", $data );
1270 case 'MANUAL':
1271 // If prefixKey is set and has a value then we use it to override the default constant XMLS_PREFIX.
1272 // If prefixKey is not set, we use the default constant XMLS_PREFIX
1273 if( isset( $this->prefixKey ) AND( $this->prefixKey !== '' ) ) {
1274 // Enable prefix override
1275 $query = str_replace( $this->prefixKey, $xmls->objectPrefix, $query );
1276 } else {
1277 // Use default replacement
1278 $query = str_replace( XMLS_PREFIX , $xmls->objectPrefix, $query );
1282 $this->queries[$id] = trim( $query );
1285 // Return the query set array
1286 return $this->queries;
1290 * Rebuilds the query with the prefix attached to any objects
1292 * @param string $regex Regex used to add prefix
1293 * @param string $query SQL query string
1294 * @param string $prefix Prefix to be appended to tables, indices, etc.
1295 * @return string Prefixed SQL query string.
1297 function prefixQuery( $regex, $query, $prefix = NULL ) {
1298 if( !isset( $prefix ) ) {
1299 return $query;
1302 if( preg_match( $regex, $query, $match ) ) {
1303 $preamble = $match[1];
1304 $postamble = $match[5];
1305 $objectList = explode( ',', $match[3] );
1306 // $prefix = $prefix . '_';
1308 $prefixedList = '';
1310 foreach( $objectList as $object ) {
1311 if( $prefixedList !== '' ) {
1312 $prefixedList .= ', ';
1315 $prefixedList .= $prefix . trim( $object );
1318 $query = $preamble . ' ' . $prefixedList . ' ' . $postamble;
1321 return $query;
1326 * Loads and parses an XML file, creating an array of "ready-to-run" SQL statements
1328 * This class is used to load and parse the XML file, to create an array of SQL statements
1329 * that can be used to build a database, and to build the database using the SQL array.
1331 * @tutorial getting_started.pkg
1333 * @author Richard Tango-Lowy & Dan Cech
1334 * @version $Revision: 1.62 $
1336 * @package axmls
1338 class adoSchema {
1341 * @var array Array containing SQL queries to generate all objects
1342 * @access private
1344 var $sqlArray;
1347 * @var object ADOdb connection object
1348 * @access private
1350 var $db;
1353 * @var object ADOdb Data Dictionary
1354 * @access private
1356 var $dict;
1359 * @var string Current XML element
1360 * @access private
1362 var $currentElement = '';
1365 * @var string If set (to 'ALTER' or 'REPLACE'), upgrade an existing database
1366 * @access private
1368 var $upgrade = '';
1371 * @var string Optional object prefix
1372 * @access private
1374 var $objectPrefix = '';
1377 * @var long System debug
1378 * @access private
1380 var $debug;
1383 * @var string Regular expression to find schema version
1384 * @access private
1386 var $versionRegex = '/<schema.*?( version="([^"]*)")?.*?>/';
1389 * @var string Current schema version
1390 * @access private
1392 var $schemaVersion;
1395 * @var int Success of last Schema execution
1397 var $success;
1400 * @var bool Execute SQL inline as it is generated
1402 var $executeInline;
1405 * @var bool Continue SQL execution if errors occur
1407 var $continueOnError;
1410 * @var int How to handle existing data rows (insert, update, or ignore)
1412 var $existingData;
1415 * Creates an adoSchema object
1417 * Creating an adoSchema object is the first step in processing an XML schema.
1418 * The only parameter is an ADOdb database connection object, which must already
1419 * have been created.
1421 * @param object $db ADOdb database connection object.
1423 function __construct( $db ) {
1424 $this->db = $db;
1425 $this->debug = $this->db->debug;
1426 $this->dict = newDataDictionary( $this->db );
1427 $this->sqlArray = array();
1428 $this->schemaVersion = XMLS_SCHEMA_VERSION;
1429 $this->executeInline( XMLS_EXECUTE_INLINE );
1430 $this->continueOnError( XMLS_CONTINUE_ON_ERROR );
1431 $this->existingData( XMLS_EXISTING_DATA );
1432 $this->setUpgradeMethod();
1436 * Sets the method to be used for upgrading an existing database
1438 * Use this method to specify how existing database objects should be upgraded.
1439 * The method option can be set to ALTER, REPLACE, BEST, or NONE. ALTER attempts to
1440 * alter each database object directly, REPLACE attempts to rebuild each object
1441 * from scratch, BEST attempts to determine the best upgrade method for each
1442 * object, and NONE disables upgrading.
1444 * This method is not yet used by AXMLS, but exists for backward compatibility.
1445 * The ALTER method is automatically assumed when the adoSchema object is
1446 * instantiated; other upgrade methods are not currently supported.
1448 * @param string $method Upgrade method (ALTER|REPLACE|BEST|NONE)
1449 * @returns string Upgrade method used
1451 function setUpgradeMethod( $method = '' ) {
1452 if( !is_string( $method ) ) {
1453 return FALSE;
1456 $method = strtoupper( $method );
1458 // Handle the upgrade methods
1459 switch( $method ) {
1460 case 'ALTER':
1461 $this->upgrade = $method;
1462 break;
1463 case 'REPLACE':
1464 $this->upgrade = $method;
1465 break;
1466 case 'BEST':
1467 $this->upgrade = 'ALTER';
1468 break;
1469 case 'NONE':
1470 $this->upgrade = 'NONE';
1471 break;
1472 default:
1473 // Use default if no legitimate method is passed.
1474 $this->upgrade = XMLS_DEFAULT_UPGRADE_METHOD;
1477 return $this->upgrade;
1481 * Specifies how to handle existing data row when there is a unique key conflict.
1483 * The existingData setting specifies how the parser should handle existing rows
1484 * when a unique key violation occurs during the insert. This can happen when inserting
1485 * data into an existing table with one or more primary keys or unique indexes.
1486 * The existingData method takes one of three options: XMLS_MODE_INSERT attempts
1487 * to always insert the data as a new row. In the event of a unique key violation,
1488 * the database will generate an error. XMLS_MODE_UPDATE attempts to update the
1489 * any existing rows with the new data based upon primary or unique key fields in
1490 * the schema. If the data row in the schema specifies no unique fields, the row
1491 * data will be inserted as a new row. XMLS_MODE_IGNORE specifies that any data rows
1492 * that would result in a unique key violation be ignored; no inserts or updates will
1493 * take place. For backward compatibility, the default setting is XMLS_MODE_INSERT,
1494 * but XMLS_MODE_UPDATE will generally be the most appropriate setting.
1496 * @param int $mode XMLS_MODE_INSERT, XMLS_MODE_UPDATE, or XMLS_MODE_IGNORE
1497 * @return int current mode
1499 function existingData( $mode = NULL ) {
1500 if( is_int( $mode ) ) {
1501 switch( $mode ) {
1502 case XMLS_MODE_UPDATE:
1503 $mode = XMLS_MODE_UPDATE;
1504 break;
1505 case XMLS_MODE_IGNORE:
1506 $mode = XMLS_MODE_IGNORE;
1507 break;
1508 case XMLS_MODE_INSERT:
1509 $mode = XMLS_MODE_INSERT;
1510 break;
1511 default:
1512 $mode = XMLS_EXISTING_DATA;
1513 break;
1515 $this->existingData = $mode;
1518 return $this->existingData;
1522 * Enables/disables inline SQL execution.
1524 * Call this method to enable or disable inline execution of the schema. If the mode is set to TRUE (inline execution),
1525 * AXMLS applies the SQL to the database immediately as each schema entity is parsed. If the mode
1526 * is set to FALSE (post execution), AXMLS parses the entire schema and you will need to call adoSchema::ExecuteSchema()
1527 * to apply the schema to the database.
1529 * @param bool $mode execute
1530 * @return bool current execution mode
1532 * @see ParseSchema(), ExecuteSchema()
1534 function executeInline( $mode = NULL ) {
1535 if( is_bool( $mode ) ) {
1536 $this->executeInline = $mode;
1539 return $this->executeInline;
1543 * Enables/disables SQL continue on error.
1545 * Call this method to enable or disable continuation of SQL execution if an error occurs.
1546 * If the mode is set to TRUE (continue), AXMLS will continue to apply SQL to the database, even if an error occurs.
1547 * If the mode is set to FALSE (halt), AXMLS will halt execution of generated sql if an error occurs, though parsing
1548 * of the schema will continue.
1550 * @param bool $mode execute
1551 * @return bool current continueOnError mode
1553 * @see addSQL(), ExecuteSchema()
1555 function continueOnError( $mode = NULL ) {
1556 if( is_bool( $mode ) ) {
1557 $this->continueOnError = $mode;
1560 return $this->continueOnError;
1564 * Loads an XML schema from a file and converts it to SQL.
1566 * Call this method to load the specified schema (see the DTD for the proper format) from
1567 * the filesystem and generate the SQL necessary to create the database
1568 * described. This method automatically converts the schema to the latest
1569 * axmls schema version.
1570 * @see ParseSchemaString()
1572 * @param string $file Name of XML schema file.
1573 * @param bool $returnSchema Return schema rather than parsing.
1574 * @return array Array of SQL queries, ready to execute
1576 function parseSchema( $filename, $returnSchema = FALSE ) {
1577 return $this->parseSchemaString( $this->convertSchemaFile( $filename ), $returnSchema );
1581 * Loads an XML schema from a file and converts it to SQL.
1583 * Call this method to load the specified schema directly from a file (see
1584 * the DTD for the proper format) and generate the SQL necessary to create
1585 * the database described by the schema. Use this method when you are dealing
1586 * with large schema files. Otherwise, parseSchema() is faster.
1587 * This method does not automatically convert the schema to the latest axmls
1588 * schema version. You must convert the schema manually using either the
1589 * convertSchemaFile() or convertSchemaString() method.
1590 * @see parseSchema()
1591 * @see convertSchemaFile()
1592 * @see convertSchemaString()
1594 * @param string $file Name of XML schema file.
1595 * @param bool $returnSchema Return schema rather than parsing.
1596 * @return array Array of SQL queries, ready to execute.
1598 * @deprecated Replaced by adoSchema::parseSchema() and adoSchema::parseSchemaString()
1599 * @see parseSchema(), parseSchemaString()
1601 function parseSchemaFile( $filename, $returnSchema = FALSE ) {
1602 // Open the file
1603 if( !($fp = fopen( $filename, 'r' )) ) {
1604 logMsg( 'Unable to open file' );
1605 return FALSE;
1608 // do version detection here
1609 if( $this->schemaFileVersion( $filename ) != $this->schemaVersion ) {
1610 logMsg( 'Invalid Schema Version' );
1611 return FALSE;
1614 if( $returnSchema ) {
1615 $xmlstring = '';
1616 while( $data = fread( $fp, 4096 ) ) {
1617 $xmlstring .= $data . "\n";
1619 return $xmlstring;
1622 $this->success = 2;
1624 $xmlParser = $this->create_parser();
1626 // Process the file
1627 while( $data = fread( $fp, 4096 ) ) {
1628 if( !xml_parse( $xmlParser, $data, feof( $fp ) ) ) {
1629 die( sprintf(
1630 "XML error: %s at line %d",
1631 xml_error_string( xml_get_error_code( $xmlParser) ),
1632 xml_get_current_line_number( $xmlParser)
1633 ) );
1637 xml_parser_free( $xmlParser );
1639 return $this->sqlArray;
1643 * Converts an XML schema string to SQL.
1645 * Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1646 * and generate the SQL necessary to create the database described by the schema.
1647 * @see parseSchema()
1649 * @param string $xmlstring XML schema string.
1650 * @param bool $returnSchema Return schema rather than parsing.
1651 * @return array Array of SQL queries, ready to execute.
1653 function parseSchemaString( $xmlstring, $returnSchema = FALSE ) {
1654 if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
1655 logMsg( 'Empty or Invalid Schema' );
1656 return FALSE;
1659 // do version detection here
1660 if( $this->SchemaStringVersion( $xmlstring ) != $this->schemaVersion ) {
1661 logMsg( 'Invalid Schema Version' );
1662 return FALSE;
1665 if( $returnSchema ) {
1666 return $xmlstring;
1669 $this->success = 2;
1671 $xmlParser = $this->create_parser();
1673 if( !xml_parse( $xmlParser, $xmlstring, TRUE ) ) {
1674 die( sprintf(
1675 "XML error: %s at line %d",
1676 xml_error_string( xml_get_error_code( $xmlParser) ),
1677 xml_get_current_line_number( $xmlParser)
1678 ) );
1681 xml_parser_free( $xmlParser );
1683 return $this->sqlArray;
1687 * Loads an XML schema from a file and converts it to uninstallation SQL.
1689 * Call this method to load the specified schema (see the DTD for the proper format) from
1690 * the filesystem and generate the SQL necessary to remove the database described.
1691 * @see RemoveSchemaString()
1693 * @param string $file Name of XML schema file.
1694 * @param bool $returnSchema Return schema rather than parsing.
1695 * @return array Array of SQL queries, ready to execute
1697 function removeSchema( $filename, $returnSchema = FALSE ) {
1698 return $this->removeSchemaString( $this->convertSchemaFile( $filename ), $returnSchema );
1702 * Converts an XML schema string to uninstallation SQL.
1704 * Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1705 * and generate the SQL necessary to uninstall the database described by the schema.
1706 * @see removeSchema()
1708 * @param string $schema XML schema string.
1709 * @param bool $returnSchema Return schema rather than parsing.
1710 * @return array Array of SQL queries, ready to execute.
1712 function removeSchemaString( $schema, $returnSchema = FALSE ) {
1714 // grab current version
1715 if( !( $version = $this->schemaStringVersion( $schema ) ) ) {
1716 return FALSE;
1719 return $this->parseSchemaString( $this->transformSchema( $schema, 'remove-' . $version), $returnSchema );
1723 * Applies the current XML schema to the database (post execution).
1725 * Call this method to apply the current schema (generally created by calling
1726 * parseSchema() or parseSchemaString() ) to the database (creating the tables, indexes,
1727 * and executing other SQL specified in the schema) after parsing.
1728 * @see parseSchema(), parseSchemaString(), executeInline()
1730 * @param array $sqlArray Array of SQL statements that will be applied rather than
1731 * the current schema.
1732 * @param boolean $continueOnErr Continue to apply the schema even if an error occurs.
1733 * @returns integer 0 if failure, 1 if errors, 2 if successful.
1735 function executeSchema( $sqlArray = NULL, $continueOnErr = NULL ) {
1736 if( !is_bool( $continueOnErr ) ) {
1737 $continueOnErr = $this->continueOnError();
1740 if( !isset( $sqlArray ) ) {
1741 $sqlArray = $this->sqlArray;
1744 if( !is_array( $sqlArray ) ) {
1745 $this->success = 0;
1746 } else {
1747 $this->success = $this->dict->executeSQLArray( $sqlArray, $continueOnErr );
1750 return $this->success;
1754 * Returns the current SQL array.
1756 * Call this method to fetch the array of SQL queries resulting from
1757 * parseSchema() or parseSchemaString().
1759 * @param string $format Format: HTML, TEXT, or NONE (PHP array)
1760 * @return array Array of SQL statements or FALSE if an error occurs
1762 function printSQL( $format = 'NONE' ) {
1763 $sqlArray = null;
1764 return $this->getSQL( $format, $sqlArray );
1768 * Saves the current SQL array to the local filesystem as a list of SQL queries.
1770 * Call this method to save the array of SQL queries (generally resulting from a
1771 * parsed XML schema) to the filesystem.
1773 * @param string $filename Path and name where the file should be saved.
1774 * @return boolean TRUE if save is successful, else FALSE.
1776 function saveSQL( $filename = './schema.sql' ) {
1778 if( !isset( $sqlArray ) ) {
1779 $sqlArray = $this->sqlArray;
1781 if( !isset( $sqlArray ) ) {
1782 return FALSE;
1785 $fp = fopen( $filename, "w" );
1787 foreach( $sqlArray as $key => $query ) {
1788 fwrite( $fp, $query . ";\n" );
1790 fclose( $fp );
1794 * Create an xml parser
1796 * @return object PHP XML parser object
1798 * @access private
1800 function create_parser() {
1801 // Create the parser
1802 $xmlParser = xml_parser_create();
1803 xml_set_object( $xmlParser, $this );
1805 // Initialize the XML callback functions
1806 xml_set_element_handler( $xmlParser, '_tag_open', '_tag_close' );
1807 xml_set_character_data_handler( $xmlParser, '_tag_cdata' );
1809 return $xmlParser;
1813 * XML Callback to process start elements
1815 * @access private
1817 function _tag_open( $parser, $tag, $attributes ) {
1818 switch( strtoupper( $tag ) ) {
1819 case 'TABLE':
1820 if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1821 $this->obj = new dbTable( $this, $attributes );
1822 xml_set_object( $parser, $this->obj );
1824 break;
1825 case 'SQL':
1826 if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1827 $this->obj = new dbQuerySet( $this, $attributes );
1828 xml_set_object( $parser, $this->obj );
1830 break;
1831 default:
1832 // print_r( array( $tag, $attributes ) );
1838 * XML Callback to process CDATA elements
1840 * @access private
1842 function _tag_cdata( $parser, $cdata ) {
1846 * XML Callback to process end elements
1848 * @access private
1849 * @internal
1851 function _tag_close( $parser, $tag ) {
1856 * Converts an XML schema string to the specified DTD version.
1858 * Call this method to convert a string containing an XML schema to a different AXMLS
1859 * DTD version. For instance, to convert a schema created for an pre-1.0 version for
1860 * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version
1861 * parameter is specified, the schema will be converted to the current DTD version.
1862 * If the newFile parameter is provided, the converted schema will be written to the specified
1863 * file.
1864 * @see convertSchemaFile()
1866 * @param string $schema String containing XML schema that will be converted.
1867 * @param string $newVersion DTD version to convert to.
1868 * @param string $newFile File name of (converted) output file.
1869 * @return string Converted XML schema or FALSE if an error occurs.
1871 function convertSchemaString( $schema, $newVersion = NULL, $newFile = NULL ) {
1873 // grab current version
1874 if( !( $version = $this->schemaStringVersion( $schema ) ) ) {
1875 return FALSE;
1878 if( !isset ($newVersion) ) {
1879 $newVersion = $this->schemaVersion;
1882 if( $version == $newVersion ) {
1883 $result = $schema;
1884 } else {
1885 $result = $this->transformSchema( $schema, 'convert-' . $version . '-' . $newVersion);
1888 if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1889 fwrite( $fp, $result );
1890 fclose( $fp );
1893 return $result;
1897 // compat for pre-4.3 - jlim
1898 function _file_get_contents($path)
1900 if (function_exists('file_get_contents')) return file_get_contents($path);
1901 return join('',file($path));
1905 * Converts an XML schema file to the specified DTD version.
1907 * Call this method to convert the specified XML schema file to a different AXMLS
1908 * DTD version. For instance, to convert a schema created for an pre-1.0 version for
1909 * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version
1910 * parameter is specified, the schema will be converted to the current DTD version.
1911 * If the newFile parameter is provided, the converted schema will be written to the specified
1912 * file.
1913 * @see convertSchemaString()
1915 * @param string $filename Name of XML schema file that will be converted.
1916 * @param string $newVersion DTD version to convert to.
1917 * @param string $newFile File name of (converted) output file.
1918 * @return string Converted XML schema or FALSE if an error occurs.
1920 function convertSchemaFile( $filename, $newVersion = NULL, $newFile = NULL ) {
1922 // grab current version
1923 if( !( $version = $this->schemaFileVersion( $filename ) ) ) {
1924 return FALSE;
1927 if( !isset ($newVersion) ) {
1928 $newVersion = $this->schemaVersion;
1931 if( $version == $newVersion ) {
1932 $result = _file_get_contents( $filename );
1934 // remove unicode BOM if present
1935 if( substr( $result, 0, 3 ) == sprintf( '%c%c%c', 239, 187, 191 ) ) {
1936 $result = substr( $result, 3 );
1938 } else {
1939 $result = $this->transformSchema( $filename, 'convert-' . $version . '-' . $newVersion, 'file' );
1942 if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1943 fwrite( $fp, $result );
1944 fclose( $fp );
1947 return $result;
1950 function transformSchema( $schema, $xsl, $schematype='string' )
1952 // Fail if XSLT extension is not available
1953 if( ! function_exists( 'xslt_create' ) ) {
1954 return FALSE;
1957 $xsl_file = dirname( __FILE__ ) . '/xsl/' . $xsl . '.xsl';
1959 // look for xsl
1960 if( !is_readable( $xsl_file ) ) {
1961 return FALSE;
1964 switch( $schematype )
1966 case 'file':
1967 if( !is_readable( $schema ) ) {
1968 return FALSE;
1971 $schema = _file_get_contents( $schema );
1972 break;
1973 case 'string':
1974 default:
1975 if( !is_string( $schema ) ) {
1976 return FALSE;
1980 $arguments = array (
1981 '/_xml' => $schema,
1982 '/_xsl' => _file_get_contents( $xsl_file )
1985 // create an XSLT processor
1986 $xh = xslt_create ();
1988 // set error handler
1989 xslt_set_error_handler ($xh, array (&$this, 'xslt_error_handler'));
1991 // process the schema
1992 $result = xslt_process ($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments);
1994 xslt_free ($xh);
1996 return $result;
2000 * Processes XSLT transformation errors
2002 * @param object $parser XML parser object
2003 * @param integer $errno Error number
2004 * @param integer $level Error level
2005 * @param array $fields Error information fields
2007 * @access private
2009 function xslt_error_handler( $parser, $errno, $level, $fields ) {
2010 if( is_array( $fields ) ) {
2011 $msg = array(
2012 'Message Type' => ucfirst( $fields['msgtype'] ),
2013 'Message Code' => $fields['code'],
2014 'Message' => $fields['msg'],
2015 'Error Number' => $errno,
2016 'Level' => $level
2019 switch( $fields['URI'] ) {
2020 case 'arg:/_xml':
2021 $msg['Input'] = 'XML';
2022 break;
2023 case 'arg:/_xsl':
2024 $msg['Input'] = 'XSL';
2025 break;
2026 default:
2027 $msg['Input'] = $fields['URI'];
2030 $msg['Line'] = $fields['line'];
2031 } else {
2032 $msg = array(
2033 'Message Type' => 'Error',
2034 'Error Number' => $errno,
2035 'Level' => $level,
2036 'Fields' => var_export( $fields, TRUE )
2040 $error_details = $msg['Message Type'] . ' in XSLT Transformation' . "\n"
2041 . '<table>' . "\n";
2043 foreach( $msg as $label => $details ) {
2044 $error_details .= '<tr><td><b>' . $label . ': </b></td><td>' . htmlentities( $details ) . '</td></tr>' . "\n";
2047 $error_details .= '</table>';
2049 trigger_error( $error_details, E_USER_ERROR );
2053 * Returns the AXMLS Schema Version of the requested XML schema file.
2055 * Call this method to obtain the AXMLS DTD version of the requested XML schema file.
2056 * @see SchemaStringVersion()
2058 * @param string $filename AXMLS schema file
2059 * @return string Schema version number or FALSE on error
2061 function schemaFileVersion( $filename ) {
2062 // Open the file
2063 if( !($fp = fopen( $filename, 'r' )) ) {
2064 // die( 'Unable to open file' );
2065 return FALSE;
2068 // Process the file
2069 while( $data = fread( $fp, 4096 ) ) {
2070 if( preg_match( $this->versionRegex, $data, $matches ) ) {
2071 return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
2075 return FALSE;
2079 * Returns the AXMLS Schema Version of the provided XML schema string.
2081 * Call this method to obtain the AXMLS DTD version of the provided XML schema string.
2082 * @see SchemaFileVersion()
2084 * @param string $xmlstring XML schema string
2085 * @return string Schema version number or FALSE on error
2087 function schemaStringVersion( $xmlstring ) {
2088 if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
2089 return FALSE;
2092 if( preg_match( $this->versionRegex, $xmlstring, $matches ) ) {
2093 return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
2096 return FALSE;
2100 * Extracts an XML schema from an existing database.
2102 * Call this method to create an XML schema string from an existing database.
2103 * If the data parameter is set to TRUE, AXMLS will include the data from the database
2104 * tables in the schema.
2106 * @param boolean $data include data in schema dump
2107 * @param string $indent indentation to use
2108 * @param string $prefix extract only tables with given prefix
2109 * @param boolean $stripprefix strip prefix string when storing in XML schema
2110 * @return string Generated XML schema
2112 function extractSchema( $data = FALSE, $indent = ' ', $prefix = '' , $stripprefix=false) {
2113 $old_mode = $this->db->setFetchMode( ADODB_FETCH_NUM );
2115 $schema = '<?xml version="1.0"?>' . "\n"
2116 . '<schema version="' . $this->schemaVersion . '">' . "\n";
2117 if( is_array( $tables = $this->db->metaTables( 'TABLES' ,false ,($prefix) ? str_replace('_','\_',$prefix).'%' : '') ) ) {
2118 foreach( $tables as $table ) {
2119 $schema .= $indent
2120 . '<table name="'
2121 . htmlentities( $stripprefix ? str_replace($prefix, '', $table) : $table )
2122 . '">' . "\n";
2124 // grab details from database
2125 $rs = $this->db->execute('SELECT * FROM ' . $table . ' WHERE 0=1');
2126 $fields = $this->db->metaColumns( $table );
2127 $indexes = $this->db->metaIndexes( $table );
2129 if( is_array( $fields ) ) {
2130 foreach( $fields as $details ) {
2131 $extra = '';
2132 $content = array();
2134 if( isset($details->max_length) && $details->max_length > 0 ) {
2135 $extra .= ' size="' . $details->max_length . '"';
2138 if( isset($details->primary_key) && $details->primary_key ) {
2139 $content[] = '<KEY/>';
2140 } elseif( isset($details->not_null) && $details->not_null ) {
2141 $content[] = '<NOTNULL/>';
2144 if( isset($details->has_default) && $details->has_default ) {
2145 $content[] = '<DEFAULT value="' . htmlentities( $details->default_value ) . '"/>';
2148 if( isset($details->auto_increment) && $details->auto_increment ) {
2149 $content[] = '<AUTOINCREMENT/>';
2152 if( isset($details->unsigned) && $details->unsigned ) {
2153 $content[] = '<UNSIGNED/>';
2156 // this stops the creation of 'R' columns,
2157 // AUTOINCREMENT is used to create auto columns
2158 $details->primary_key = 0;
2159 $type = $rs->metaType( $details );
2161 $schema .= str_repeat( $indent, 2 ) . '<field name="' . htmlentities( $details->name ) . '" type="' . $type . '"' . $extra;
2163 if( !empty( $content ) ) {
2164 $schema .= ">\n" . str_repeat( $indent, 3 )
2165 . implode( "\n" . str_repeat( $indent, 3 ), $content ) . "\n"
2166 . str_repeat( $indent, 2 ) . '</field>' . "\n";
2167 } else {
2168 $schema .= "/>\n";
2173 if( is_array( $indexes ) ) {
2174 foreach( $indexes as $index => $details ) {
2175 $schema .= str_repeat( $indent, 2 ) . '<index name="' . $index . '">' . "\n";
2177 if( $details['unique'] ) {
2178 $schema .= str_repeat( $indent, 3 ) . '<UNIQUE/>' . "\n";
2181 foreach( $details['columns'] as $column ) {
2182 $schema .= str_repeat( $indent, 3 ) . '<col>' . htmlentities( $column ) . '</col>' . "\n";
2185 $schema .= str_repeat( $indent, 2 ) . '</index>' . "\n";
2189 if( $data ) {
2190 $rs = $this->db->execute( 'SELECT * FROM ' . $table );
2192 if( is_object( $rs ) && !$rs->EOF ) {
2193 $schema .= str_repeat( $indent, 2 ) . "<data>\n";
2195 while( $row = $rs->fetchRow() ) {
2196 foreach( $row as $key => $val ) {
2197 if ( $val != htmlentities( $val ) ) {
2198 $row[$key] = '<![CDATA[' . $val . ']]>';
2202 $schema .= str_repeat( $indent, 3 ) . '<row><f>' . implode( '</f><f>', $row ) . "</f></row>\n";
2205 $schema .= str_repeat( $indent, 2 ) . "</data>\n";
2209 $schema .= $indent . "</table>\n";
2213 $this->db->setFetchMode( $old_mode );
2215 $schema .= '</schema>';
2216 return $schema;
2220 * Sets a prefix for database objects
2222 * Call this method to set a standard prefix that will be prepended to all database tables
2223 * and indices when the schema is parsed. Calling setPrefix with no arguments clears the prefix.
2225 * @param string $prefix Prefix that will be prepended.
2226 * @param boolean $underscore If TRUE, automatically append an underscore character to the prefix.
2227 * @return boolean TRUE if successful, else FALSE
2229 function setPrefix( $prefix = '', $underscore = TRUE ) {
2230 switch( TRUE ) {
2231 // clear prefix
2232 case empty( $prefix ):
2233 logMsg( 'Cleared prefix' );
2234 $this->objectPrefix = '';
2235 return TRUE;
2236 // prefix too long
2237 case strlen( $prefix ) > XMLS_PREFIX_MAXLEN:
2238 // prefix contains invalid characters
2239 case !preg_match( '/^[a-z][a-z0-9_]+$/i', $prefix ):
2240 logMsg( 'Invalid prefix: ' . $prefix );
2241 return FALSE;
2244 if( $underscore AND substr( $prefix, -1 ) != '_' ) {
2245 $prefix .= '_';
2248 // prefix valid
2249 logMsg( 'Set prefix: ' . $prefix );
2250 $this->objectPrefix = $prefix;
2251 return TRUE;
2255 * Returns an object name with the current prefix prepended.
2257 * @param string $name Name
2258 * @return string Prefixed name
2260 * @access private
2262 function prefix( $name = '' ) {
2263 // if prefix is set
2264 if( !empty( $this->objectPrefix ) ) {
2265 // Prepend the object prefix to the table name
2266 // prepend after quote if used
2267 return preg_replace( '/^(`?)(.+)$/', '$1' . $this->objectPrefix . '$2', $name );
2270 // No prefix set. Use name provided.
2271 return $name;
2275 * Checks if element references a specific platform
2277 * @param string $platform Requested platform
2278 * @returns boolean TRUE if platform check succeeds
2280 * @access private
2282 function supportedPlatform( $platform = NULL ) {
2283 if( !empty( $platform ) ) {
2284 $regex = '/(^|\|)' . $this->db->databaseType . '(\||$)/i';
2286 if( preg_match( '/^- /', $platform ) ) {
2287 if (preg_match ( $regex, substr( $platform, 2 ) ) ) {
2288 logMsg( 'Platform ' . $platform . ' is NOT supported' );
2289 return FALSE;
2291 } else {
2292 if( !preg_match ( $regex, $platform ) ) {
2293 logMsg( 'Platform ' . $platform . ' is NOT supported' );
2294 return FALSE;
2299 logMsg( 'Platform ' . $platform . ' is supported' );
2300 return TRUE;
2304 * Clears the array of generated SQL.
2306 * @access private
2308 function clearSQL() {
2309 $this->sqlArray = array();
2313 * Adds SQL into the SQL array.
2315 * @param mixed $sql SQL to Add
2316 * @return boolean TRUE if successful, else FALSE.
2318 * @access private
2320 function addSQL( $sql = NULL ) {
2321 if( is_array( $sql ) ) {
2322 foreach( $sql as $line ) {
2323 $this->addSQL( $line );
2326 return TRUE;
2329 if( is_string( $sql ) ) {
2330 $this->sqlArray[] = $sql;
2332 // if executeInline is enabled, and either no errors have occurred or continueOnError is enabled, execute SQL.
2333 if( $this->ExecuteInline() && ( $this->success == 2 || $this->ContinueOnError() ) ) {
2334 $saved = $this->db->debug;
2335 $this->db->debug = $this->debug;
2336 $ok = $this->db->Execute( $sql );
2337 $this->db->debug = $saved;
2339 if( !$ok ) {
2340 if( $this->debug ) {
2341 ADOConnection::outp( $this->db->ErrorMsg() );
2344 $this->success = 1;
2348 return TRUE;
2351 return FALSE;
2355 * Gets the SQL array in the specified format.
2357 * @param string $format Format
2358 * @return mixed SQL
2360 * @access private
2362 function getSQL( $format = NULL, $sqlArray = NULL ) {
2363 if( !is_array( $sqlArray ) ) {
2364 $sqlArray = $this->sqlArray;
2367 if( !is_array( $sqlArray ) ) {
2368 return FALSE;
2371 switch( strtolower( $format ) ) {
2372 case 'string':
2373 case 'text':
2374 return !empty( $sqlArray ) ? implode( ";\n\n", $sqlArray ) . ';' : '';
2375 case'html':
2376 return !empty( $sqlArray ) ? nl2br( htmlentities( implode( ";\n\n", $sqlArray ) . ';' ) ) : '';
2379 return $this->sqlArray;
2383 * Destroys an adoSchema object.
2385 * Call this method to clean up after an adoSchema object that is no longer in use.
2386 * @deprecated adoSchema now cleans up automatically.
2388 function destroy() {
2393 * Message logging function
2395 * @access private
2397 function logMsg( $msg, $title = NULL, $force = FALSE ) {
2398 if( XMLS_DEBUG or $force ) {
2399 echo '<pre>';
2401 if( isset( $title ) ) {
2402 echo '<h3>' . htmlentities( $title ) . '</h3>';
2405 if( @is_object( $this ) ) {
2406 echo '[' . get_class( $this ) . '] ';
2409 print_r( $msg );
2411 echo '</pre>';