aboutsummaryrefslogtreecommitdiffstats
path: root/po/af.po
blob: c50cad290a8740b7f5eaa008da066c8858f49699 (plain)
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# translation of userdrake-af.po to Afrikaans
# translation of userdrake.po to
# Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc.
# Dirk Konrad <dirkvanderwalt@webmail.co.za>, 2003.
# Dirk van der Walt <dirkvanderwalt@webmail.co.za>, 2004.
# Die uwe Ysbeer <ysbeer@af.org.za>, 2005.
#
msgid ""
msgstr ""
"Project-Id-Version: userdrake-af\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2005-02-09 13:30+0100\n"
"PO-Revision-Date: 2005-02-08 21:19+0000\n"
"Last-Translator: Die uwe Ysbeer <ysbeer@af.org.za>\n"
"Language-Team: Afrikaans <i18n@af.org.za>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.9.1\n"

#: ../USER/USER.xs:84
#, c-format
msgid "Error reading `%s': %s"
msgstr "Fout met lees van %s: %s"

#: ../USER/USER.xs:91
#, c-format
msgid "Error creating `%s': %s"
msgstr "Fout met skep van %s: %s"

#: ../USER/USER.xs:203 ../USER/USER.xs:232 ../USER/USER.xs:248
#, c-format
msgid "Error removing `%s': %s"
msgstr "Fout met verwydering van %s: %s"

#: ../USER/USER.xs:531
msgid "Error creating mail spool.\n"
msgstr "Kon nie spoellêer skep nie.\n"

#: ../USER/USER.xs:617
msgid "unknown error"
msgstr "onbekende fout"

#: ../USER/USER.xs:880
msgid "Error deleting mail spool.\n"
msgstr "Fout met uitvee van spoellêer.\n"

#: ../userdrake:55 ../userdrake:1010
#, c-format
msgid "Userdrake"
msgstr "Userdrake"

#: ../userdrake:60
#, c-format
msgid "Loading Users and Groups... Please wait"
msgstr "Opsoek na Gerbruikers en Groepe .... Net 'n oomblik asb"

#: ../userdrake:79
#, c-format
msgid "Mandrakelinux Users Management Tool"
msgstr "Mandrakelinux Users Management Tool"

#: ../userdrake:89 ../userdrake:90 ../userdrake:98 ../userdrake:99
#: ../userdrake:100
#, c-format
msgid "/_Actions"
msgstr "/_Aksies"

#: ../userdrake:89
#, c-format
msgid "/_Edit"
msgstr "/_Redigeer"

#: ../userdrake:90
#, c-format
msgid "/_Delete"
msgstr "/_Skrap"

#: ../userdrake:91 ../userdrake:103
#, c-format
msgid "/_Options"
msgstr "/_Opsies"

#: ../userdrake:91
#, c-format
msgid "/_Filter system users"
msgstr "/_Filtreer 'system' gebruikers"

#: ../userdrake:95 ../userdrake:96 ../userdrake:97
#, c-format
msgid "/_File"
msgstr "/_Lêer"

#: ../userdrake:96
#, c-format
msgid "/_Refresh"
msgstr "/_Herlaai"

#: ../userdrake:97
#, c-format
msgid "/_Quit"
msgstr "_Verlaat"

#: ../userdrake:97
#, c-format
msgid "<control>Q"
msgstr "<control>Q"

#: ../userdrake:99
#, c-format
msgid "/_Add User"
msgstr "/_Voeg Gebruiker By"

#: ../userdrake:100
#, c-format
msgid "/Add _Group"
msgstr "/Voeg _Groep by"

#: ../userdrake:108 ../userdrake:109 ../userdrake:110 ../userdrake:111
#, c-format
msgid "/_Help"
msgstr "/_Help"

#: ../userdrake:110
#, c-format
msgid "/_Report Bug"
msgstr "/_Raporteer 'n Fout"

#: ../userdrake:111
#, c-format
msgid "/_About..."
msgstr "/_Otrent"

#: ../userdrake:126
#, c-format
msgid "Search:"
msgstr "Soek:"

#: ../userdrake:129
#, c-format
msgid "Apply filter"
msgstr "Pas filter toe"

#: ../userdrake:131
#, c-format
msgid "Users"
msgstr "Gebruikers"

#: ../userdrake:133 ../userdrake:733
#, c-format
msgid "Groups"
msgstr "Groepe"

#: ../userdrake:154
#, c-format
msgid "User Name"
msgstr "Gebruikernaam"

#: ../userdrake:154
#, c-format
msgid "User ID"
msgstr "Gebruiker ID"

#: ../userdrake:154 ../userdrake:730
#, c-format
msgid "Primary Group"
msgstr "Primere Groep"

#: ../userdrake:154 ../userdrake:640
#, c-format
msgid "Full Name"
msgstr "Volle Naam"

#: ../userdrake:154 ../userdrake:644
#, c-format
msgid "Login Shell"
msgstr "Aanteken Dop"

#: ../userdrake:154
#, c-format
msgid "Home Directory"
msgstr "Tuisgids"

#: ../userdrake:154
#, c-format
msgid "Status"
msgstr "Stand"

#: ../userdrake:161 ../userdrake:655
#, c-format
msgid "Group Name"
msgstr "Groep Naam"

#: ../userdrake:161
#, c-format
msgid "Group ID"
msgstr "Groep ID"

#: ../userdrake:161
#, c-format
msgid "Group Members"
msgstr "Group Lede"

#: ../userdrake:163
#, c-format
msgid "Add User"
msgstr "Voeg Gebruiker By"

#: ../userdrake:163
#, c-format
msgid "Add a user to the system"
msgstr "Voeg 'n gebruiker by die stelsel"

#: ../userdrake:164
#, c-format
msgid "Add Group"
msgstr "Voeg Groep By"

#: ../userdrake:164
#, c-format
msgid "Add a group to the system"
msgstr "Voeg 'n groep by die stelsel"

#: ../userdrake:165 ../userdrake:593
#, c-format
msgid "Edit"
msgstr "Redigeer"

#: ../userdrake:165
#, c-format
msgid "Edit selected row"
msgstr "Redigeer gekose ry"

#: ../userdrake:166 ../userdrake:544 ../userdrake:575 ../userdrake:593
#, c-format
msgid "Delete"
msgstr "Verwyder"

#: ../userdrake:166
#, c-format
msgid "Delete selected row"
msgstr "Wis gekose ry"

#: ../userdrake:167
#, c-format
msgid "Refresh"
msgstr "Herlaai"

#: ../userdrake:167
#, c-format
msgid "Refresh the list"
msgstr "Herlaai die lys"

#: ../userdrake:204
#, c-format
msgid "Locked"
msgstr "Gesluit"

#: ../userdrake:204
#, c-format
msgid "Expired"
msgstr "Reeds Verval"

#: ../userdrake:258
#, c-format
msgid "Click on the icon to change it"
msgstr "Kliek op die ikoon om dit te verander"

#: ../userdrake:299
#, c-format
msgid "Create New User"
msgstr "Skep Nuwe Gebruiker"

#: ../userdrake:306
#, c-format
msgid "Create Home Directory"
msgstr "Skep Tuisgids"

#: ../userdrake:308
#, c-format
msgid "Home Directory: "
msgstr "Tuisgids: "

#: ../userdrake:311
#, c-format
msgid "Create a private group for the user"
msgstr "Skep 'n unieke groep vir die gebruiker"

#: ../userdrake:312
#, c-format
msgid "Specify user ID manually"
msgstr "Spesifiseer gebruiker se ID self"

#: ../userdrake:326
#, c-format
msgid "User already exists, please choose another User Name"
msgstr "Gebruiker bestaan alreeds, kies asb. 'n ander gebruikernaam"

#: ../userdrake:328 ../userdrake:808
#, c-format
msgid "Password Mismatch"
msgstr "Ongepaarde Wagwoord"

#: ../userdrake:329 ../userdrake:810
#, c-format
msgid ""
"This password is too simple. \n"
" Good passwords should be > 6 characters"
msgstr ""
"Hierdie wagwoord is te eenvoudig. \n"
"Behoorlike wagwoorde bevat meer as 6 karakters "

#: ../userdrake:340
#, c-format
msgid "User Uid is < 500"
msgstr "Gebruiker UID is < 500"

#: ../userdrake:340
#, c-format
msgid ""
"Creating a user with a UID less than 500 is not recommended.\n"
" Are you sure you want to do this?\n"
"\n"
msgstr ""
"Die skep van 'n groep met 'n UID kleiner as 500 word nie aanbeveel nie.\n"
"Wil u voort gaan?\n"
"\n"

#: ../userdrake:355
#, c-format
msgid "Putting %s to 'users' group"
msgstr "Voeg %s by die 'users' groep"

#: ../userdrake:361
#, c-format
msgid "Creating new group: %s"
msgstr "Skep nuwe groep: %s"

#: ../userdrake:370
#, c-format
msgid "Adding user : %s"
msgstr "Voeg gebruiker: %s by."

#: ../userdrake:382 ../userdrake:421 ../userdrake:465 ../userdrake:544
#: ../userdrake:575 ../userdrake:912
#, c-format
msgid "Cancel"
msgstr "Kanselleer"

#: ../userdrake:382 ../userdrake:416 ../userdrake:465 ../userdrake:912
#: ../userdrake:1046
#, c-format
msgid "Ok"
msgstr "OK"

#: ../userdrake:395
#, c-format
msgid "UID: "
msgstr "UID: "

#: ../userdrake:407
#, c-format
msgid "Choose group"
msgstr "Kies Groep"

#: ../userdrake:409
#, c-format
msgid "Add to the existing group"
msgstr "Voeg by die bestaande groep"

#: ../userdrake:409
#, c-format
msgid "Add to the 'users' group"
msgstr "Voeg by die 'users' groep"

#: ../userdrake:412
#, c-format
msgid "A group with this name already exists.  What would you like to do?"
msgstr "'n Groep met hierdie naam bestaan reeds. Wat wil u graag doen?"

#: ../userdrake:433
#, c-format
msgid "Create New Group"
msgstr "Skep Nuwe Groep"

#: ../userdrake:439
#, c-format
msgid "Specify group ID manually"
msgstr "Spesifiseer groep se ID self"

#: ../userdrake:450
#, c-format
msgid "Group already exists, please choose another Group Name"
msgstr "Groep bestaan alreeds, Gebruik asb. 'n ander Groepnaam"

#: ../userdrake:454
#, c-format
msgid " Group Gid is < 500"
msgstr "Groep GID is < 500"

#: ../userdrake:454
#, c-format
msgid ""
"Creating a group with a GID less than 500 is not recommended.\n"
" Are you sure you want to do this?\n"
"\n"
msgstr ""
"Die skep van 'n groep met 'n GID kleiner as 500 word nie aanbeveel nie.\n"
"Moet ons voort gaan?\n"
"\n"

#: ../userdrake:459
#, c-format
msgid "Adding group : %s "
msgstr "Voeg groep %s by"

#: ../userdrake:470
#, c-format
msgid "GID: "
msgstr "GID: "

#: ../userdrake:515 ../userdrake:551
#, c-format
msgid "Delete files or not?"
msgstr "Wis leêrs uit of nie?"

#: ../userdrake:518
#, c-format
msgid ""
"Deleting user %s\n"
" Also perform the following actions\n"
msgstr ""
"Verwyder gebruiker %s\n"
" Doen ook die volgende\n"

#: ../userdrake:519
#, c-format
msgid "Delete Home Directory :%s"
msgstr "Verwyder Tuisgids: %s"

#: ../userdrake:520
#, c-format
msgid "Delete Mailbox :/var/spool/mail/%s"
msgstr "Verwyder Posbus : /var/spool/mail/%s"

#: ../userdrake:527
#, c-format
msgid "Removing user: %s"
msgstr "Verwyder gebruiker: %s"

#: ../userdrake:554
#, c-format
msgid "Do you really want to delete the group %s\n"
msgstr "Moet ons regtig groep %s verwyder\n"

#: ../userdrake:566
#, c-format
msgid ""
"%s is a primary group for user %s\n"
" Remove the user first"
msgstr ""
"%s is 'n primere groep vir gebruiker %s\n"
" Verwyder eers die gebruiker"

#: ../userdrake:571
#, c-format
msgid "Removing group: %s"
msgstr "Verwyder groep: %s"

#: ../userdrake:641
#, c-format
msgid "Login"
msgstr "Inteken"

#: ../userdrake:642
#, c-format
msgid "Password"
msgstr "Wagwoord"

#: ../userdrake:643
#, c-format
msgid "Confirm Password:"
msgstr "Bevestig Wagwoord"

#: ../userdrake:670
#, c-format
msgid "Edit Groups / Users"
msgstr "Redigeer Groepe / Gebruikers"

#: ../userdrake:690
#, c-format
msgid "Home"
msgstr "Tuis"

#: ../userdrake:692
#, c-format
msgid "User Data"
msgstr "Gebruiker se Inligting"

#: ../userdrake:695
#, c-format
msgid "Enable account expiration"
msgstr "Aktiveer verval van rekening"

#: ../userdrake:697
#, c-format
msgid "Account expires (YYYY-MM-DD):"
msgstr "Rekening verval (YYYY-MM-DD):"

#: ../userdrake:703
#, c-format
msgid "Lock User Account"
msgstr "Sluit Gebruiker se Rekening"

#: ../userdrake:706
#, c-format
msgid "Account Info"
msgstr "Rekening Inligting"

#: ../userdrake:711
#, c-format
msgid "User last changed password on : "
msgstr "Gebruiker het laas sy/haar wagwoord verander op : "

#: ../userdrake:715
#, c-format
msgid "Enable Password Expiration"
msgstr "Aktiveer Verval van Wagwoord"

#: ../userdrake:718
#, c-format
msgid "Days before change allowed :"
msgstr "Dae voor verandering toelaat :"

#: ../userdrake:719
#, c-format
msgid "Days before change required :"
msgstr "Aantal dae voor verandering verlang :"

#: ../userdrake:720
#, c-format
msgid "Days warning before change :"
msgstr "Waarsku soveel dae voor verandering :"

#: ../userdrake:721
#, c-format
msgid "Days before account inactive :"
msgstr "Dae voor rekening de-aktiveer :"

#: ../userdrake:723
#, c-format
msgid "Password Info"
msgstr "Wagwoord Detail"

#: ../userdrake:727
#, c-format
msgid "Select the groups that the user will be a member of:"
msgstr "Kies die groepe waarvan die gebruiker 'n lid moet wees:"

#: ../userdrake:772
#, c-format
msgid "Group Data"
msgstr "Groep Data"

#: ../userdrake:775
#, c-format
msgid "Select the users to join this group :"
msgstr "Kies gebruikers vir hierdie groep :"

#: ../userdrake:776
#, c-format
msgid "Group Users"
msgstr "Groepeer Gebruikers"

#: ../userdrake:838
#, c-format
msgid "Please select at least one group for the user"
msgstr "Kies asseblief ten minste een groep vir gebruiker"

#: ../userdrake:847
#, c-format
msgid ""
"Please specify Year, Month and Day \n"
" for Account Expiration "
msgstr ""
"Spesifiseer asseblief Jaar Maand en Dag \n"
"waarop Rekening Verval "

#: ../userdrake:853
#, c-format
msgid "Please fill up all fields in password aging\n"
msgstr ""
"Voorsien asseblief al die verlangde inling rakende wagwoord veroudering\n"

#: ../userdrake:896
#, c-format
msgid "You cannot remove user '%s' from their primary group"
msgstr "U kan nie gebruiker %s van hulle primere groep verwyder nie"

#: ../userdrake:1003
#, c-format
msgid "Close"
msgstr "Maak Toe"

#: ../userdrake:1016
#, c-format
msgid "Authors: "
msgstr "Programeerders: "

#: ../userdrake:1022
#, c-format
msgid "Users Management \n"
msgstr "Bestuur Gebruikers \n"

#: ../userdrake:1028
#, c-format
msgid "Name field is empty please provide a name"
msgstr "Naam spasie is leeg voorsien asseblief 'n naam"

#: ../userdrake:1029
#, c-format
msgid ""
"The name must contain only lower cased latin letters, numbers, `-' and `_'"
msgstr "Die naam mag sleg alfabet letters, syfers, '-' en '_' bevat"

#: ../userdrake:1030
#, c-format
msgid "Name is too long"
msgstr "Naam is te lank"

#: ../userdrake:1040
#, c-format
msgid "Error"
msgstr "Probleme"
='#n1794'>1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Andrew Dunstan <andrew@dunslane.net>,
#                 Edward J. Sabol <edwardjsabol@iname.com>
#                 Max Kanat-Alexander <mkanat@bugzilla.org>
#                 Lance Larsh <lance.larsh@oracle.com>
#                 Dennis Melentyev <dennis.melentyev@infopulse.com.ua>
#                 Akamai Technologies <bugzilla-dev@akamai.com>

package Bugzilla::DB::Schema;

###########################################################################
#
# Purpose: Object-oriented, DBMS-independent database schema for Bugzilla
#
# This is the base class implementing common methods and abstract schema.
#
###########################################################################

use strict;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util;
use Bugzilla::Constants;

use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys);
use Safe;
# Historical, needed for SCHEMA_VERSION = '1.00'
use Storable qw(dclone freeze thaw);

# New SCHEMA_VERSION (2.00) use this
use Data::Dumper;

=head1 NAME

Bugzilla::DB::Schema - Abstract database schema for Bugzilla

=head1 SYNOPSIS

  # Obtain MySQL database schema.
  # Do not do this. Use Bugzilla::DB instead.
  use Bugzilla::DB::Schema;
  my $mysql_schema = new Bugzilla::DB::Schema('Mysql');

  # Recommended way to obtain database schema.
  use Bugzilla::DB;
  my $dbh = Bugzilla->dbh;
  my $schema = $dbh->_bz_schema();

  # Get the list of tables in the Bugzilla database.
  my @tables = $schema->get_table_list();

  # Get the SQL statements need to create the bugs table.
  my @statements = $schema->get_table_ddl('bugs');

  # Get the database-specific SQL data type used to implement
  # the abstract data type INT1.
  my $db_specific_type = $schema->sql_type('INT1');

=head1 DESCRIPTION

This module implements an object-oriented, abstract database schema.
It should be considered package-private to the Bugzilla::DB module.
That means that CGI scripts should never call any function in this
module directly, but should instead rely on methods provided by
Bugzilla::DB.

=head1 NEW TO SCHEMA.PM?

If this is your first time looking at Schema.pm, especially if
you are making changes to the database, please take a look at
L<http://www.bugzilla.org/docs/developer.html#sql-schema> to learn
more about how this integrates into the rest of Bugzilla.

=cut

#--------------------------------------------------------------------------
# Define the Bugzilla abstract database schema and version as constants.

=head1 CONSTANTS

=over 4

=item C<SCHEMA_VERSION>

The 'version' of the internal schema structure. This version number
is incremented every time the the fundamental structure of Schema
internals changes.

This is NOT changed every time a table or a column is added. This 
number is incremented only if the internal structures of this 
Schema would be incompatible with the internal structures of a 
previous Schema version.

In general, unless you are messing around with serialization
and deserialization of the schema, you don't need to worry about
this constant.

=begin private

An example of the use of the version number:

Today, we store all individual columns like this:

column_name => { TYPE => 'SOMETYPE', NOTNULL => 1 }

Imagine that someday we decide that NOTNULL => 1 is bad, and we want
to change it so that the schema instead uses NULL => 0.

But we have a bunch of Bugzilla installations around the world with a
serialized schema that has NOTNULL in it! When we deserialize that 
structure, it just WILL NOT WORK properly inside of our new Schema object.
So, immediately after deserializing, we need to go through the hash 
and change all NOTNULLs to NULLs and so on.

We know that we need to do that on deserializing because we know that
version 1.00 used NOTNULL. Having made the change to NULL, we would now
be version 1.01.

=end private

=item C<ABSTRACT_SCHEMA>

The abstract database schema structure consists of a hash reference
in which each key is the name of a table in the Bugzilla database.

The value for each key is a hash reference containing the keys
C<FIELDS> and C<INDEXES> which in turn point to array references
containing information on the table's fields and indexes. 

A field hash reference should must contain the key C<TYPE>. Optional field
keys include C<PRIMARYKEY>, C<NOTNULL>, and C<DEFAULT>. 

The C<INDEXES> array reference contains index names and information 
regarding the index. If the index name points to an array reference,
then the index is a regular index and the array contains the indexed
columns. If the index name points to a hash reference, then the hash
must contain the key C<FIELDS>. It may also contain the key C<TYPE>,
which can be used to specify the type of index such as UNIQUE or FULLTEXT.

=back

=cut

use constant SCHEMA_VERSION  => '2.00';
use constant ABSTRACT_SCHEMA => {

    # BUG-RELATED TABLES
    # ------------------

    # General Bug Information
    # -----------------------
    bugs => {
        FIELDS => [
            bug_id              => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                                    PRIMARYKEY => 1},
            assigned_to         => {TYPE => 'INT3', NOTNULL => 1},
            bug_file_loc        => {TYPE => 'TEXT'},
            bug_severity        => {TYPE => 'varchar(64)', NOTNULL => 1},
            bug_status          => {TYPE => 'varchar(64)', NOTNULL => 1},
            creation_ts         => {TYPE => 'DATETIME'},
            delta_ts            => {TYPE => 'DATETIME', NOTNULL => 1},
            short_desc          => {TYPE => 'varchar(255)', NOTNULL => 1},
            op_sys              => {TYPE => 'varchar(64)', NOTNULL => 1},
            priority            => {TYPE => 'varchar(64)', NOTNULL => 1},
            product_id          => {TYPE => 'INT2', NOTNULL => 1},
            rep_platform        => {TYPE => 'varchar(64)', NOTNULL => 1},
            reporter            => {TYPE => 'INT3', NOTNULL => 1},
            version             => {TYPE => 'varchar(64)', NOTNULL => 1},
            component_id        => {TYPE => 'INT2', NOTNULL => 1},
            resolution          => {TYPE => 'varchar(64)',
                                    NOTNULL => 1, DEFAULT => "''"},
            target_milestone    => {TYPE => 'varchar(20)',
                                    NOTNULL => 1, DEFAULT => "'---'"},
            qa_contact          => {TYPE => 'INT3'},
            status_whiteboard   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
                                    DEFAULT => "''"},
            votes               => {TYPE => 'INT3', NOTNULL => 1,
                                    DEFAULT => '0'},
            # Note: keywords field is only a cache; the real data
            # comes from the keywords table
            keywords            => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
                                    DEFAULT => "''"},
            lastdiffed          => {TYPE => 'DATETIME'},
            everconfirmed       => {TYPE => 'BOOLEAN', NOTNULL => 1},
            reporter_accessible => {TYPE => 'BOOLEAN',
                                    NOTNULL => 1, DEFAULT => 'TRUE'},
            cclist_accessible   => {TYPE => 'BOOLEAN',
                                    NOTNULL => 1, DEFAULT => 'TRUE'},
            estimated_time      => {TYPE => 'decimal(5,2)',
                                    NOTNULL => 1, DEFAULT => '0'},
            remaining_time      => {TYPE => 'decimal(5,2)',
                                    NOTNULL => 1, DEFAULT => '0'},
            deadline            => {TYPE => 'DATETIME'},
            alias               => {TYPE => 'varchar(20)'},
        ],
        INDEXES => [
            bugs_alias_idx            => {FIELDS => ['alias'],
                                          TYPE => 'UNIQUE'},
            bugs_assigned_to_idx      => ['assigned_to'],
            bugs_creation_ts_idx      => ['creation_ts'],
            bugs_delta_ts_idx         => ['delta_ts'],
            bugs_bug_severity_idx     => ['bug_severity'],
            bugs_bug_status_idx       => ['bug_status'],
            bugs_op_sys_idx           => ['op_sys'],
            bugs_priority_idx         => ['priority'],
            bugs_product_id_idx       => ['product_id'],
            bugs_reporter_idx         => ['reporter'],
            bugs_version_idx          => ['version'],
            bugs_component_id_idx     => ['component_id'],
            bugs_resolution_idx       => ['resolution'],
            bugs_target_milestone_idx => ['target_milestone'],
            bugs_qa_contact_idx       => ['qa_contact'],
            bugs_votes_idx            => ['votes'],
        ],
    },

    bugs_activity => {
        FIELDS => [
            bug_id    => {TYPE => 'INT3', NOTNULL => 1},
            attach_id => {TYPE => 'INT3'},
            who       => {TYPE => 'INT3', NOTNULL => 1},
            bug_when  => {TYPE => 'DATETIME', NOTNULL => 1},
            fieldid   => {TYPE => 'INT3', NOTNULL => 1},
            added     => {TYPE => 'TINYTEXT'},
            removed   => {TYPE => 'TINYTEXT'},
        ],
        INDEXES => [
            bugs_activity_bug_id_idx  => ['bug_id'],
            bugs_activity_who_idx     => ['who'],
            bugs_activity_bug_when_idx => ['bug_when'],
            bugs_activity_fieldid_idx => ['fieldid'],
        ],
    },

    cc => {
        FIELDS => [
            bug_id => {TYPE => 'INT3', NOTNULL => 1},
            who    => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            cc_bug_id_idx => {FIELDS => [qw(bug_id who)],
                              TYPE => 'UNIQUE'},
            cc_who_idx    => ['who'],
        ],
    },

    longdescs => {
        FIELDS => [
            comment_id      => {TYPE => 'MEDIUMSERIAL',  NOTNULL => 1,
                                PRIMARYKEY => 1},
            bug_id          => {TYPE => 'INT3',  NOTNULL => 1},
            who             => {TYPE => 'INT3', NOTNULL => 1},
            bug_when        => {TYPE => 'DATETIME', NOTNULL => 1},
            work_time       => {TYPE => 'decimal(5,2)', NOTNULL => 1,
                                DEFAULT => '0'},
            thetext         => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            isprivate       => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                DEFAULT => 'FALSE'},
            already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                DEFAULT => 'FALSE'},
        ],
        INDEXES => [
            longdescs_bug_id_idx   => ['bug_id'],
            longdescs_who_idx     => [qw(who bug_id)],
            longdescs_bug_when_idx => ['bug_when'],
            longdescs_thetext_idx => {FIELDS => ['thetext'],
                                      TYPE => 'FULLTEXT'},
        ],
    },

    dependencies => {
        FIELDS => [
            blocked   => {TYPE => 'INT3', NOTNULL => 1},
            dependson => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            dependencies_blocked_idx   => ['blocked'],
            dependencies_dependson_idx => ['dependson'],
        ],
    },

    votes => {
        FIELDS => [
            who        => {TYPE => 'INT3', NOTNULL => 1},
            bug_id     => {TYPE => 'INT3', NOTNULL => 1},
            vote_count => {TYPE => 'INT2', NOTNULL => 1},
        ],
        INDEXES => [
            votes_who_idx    => ['who'],
            votes_bug_id_idx => ['bug_id'],
        ],
    },

    attachments => {
        FIELDS => [
            attach_id    => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                             PRIMARYKEY => 1},
            bug_id       => {TYPE => 'INT3', NOTNULL => 1},
            creation_ts  => {TYPE => 'DATETIME', NOTNULL => 1},
            description  => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            mimetype     => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            ispatch      => {TYPE => 'BOOLEAN'},
            filename     => {TYPE => 'varchar(100)', NOTNULL => 1},
            submitter_id => {TYPE => 'INT3', NOTNULL => 1},
            isobsolete   => {TYPE => 'BOOLEAN', NOTNULL => 1,
                             DEFAULT => 'FALSE'},
            isprivate    => {TYPE => 'BOOLEAN', NOTNULL => 1,
                             DEFAULT => 'FALSE'},
            isurl        => {TYPE => 'BOOLEAN', NOTNULL => 1,
                             DEFAULT => 'FALSE'},
        ],
        INDEXES => [
            attachments_bug_id_idx => ['bug_id'],
            attachments_creation_ts_idx => ['creation_ts'],
            attachments_submitter_id_idx => ['submitter_id', 'bug_id'],
        ],
    },
    attach_data => {
        FIELDS => [
            id      => {TYPE => 'INT3', NOTNULL => 1,
                        PRIMARYKEY => 1},
            thedata => {TYPE => 'LONGBLOB', NOTNULL => 1},
        ],
    },

    duplicates => {
        FIELDS => [
            dupe_of => {TYPE => 'INT3', NOTNULL => 1},
            dupe    => {TYPE => 'INT3', NOTNULL => 1,
                       PRIMARYKEY => 1},
        ],
    },

    # Keywords
    # --------

    keyworddefs => {
        FIELDS => [
            id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                            PRIMARYKEY => 1},
            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
            description => {TYPE => 'MEDIUMTEXT'},
        ],
        INDEXES => [
            keyworddefs_name_idx   => {FIELDS => ['name'],
                                       TYPE => 'UNIQUE'},
        ],
    },

    keywords => {
        FIELDS => [
            bug_id    => {TYPE => 'INT3', NOTNULL => 1},
            keywordid => {TYPE => 'INT2', NOTNULL => 1},
        ],
        INDEXES => [
            keywords_bug_id_idx    => {FIELDS => [qw(bug_id keywordid)],
                                       TYPE => 'UNIQUE'},
            keywords_keywordid_idx => ['keywordid'],
        ],
    },

    # Flags
    # -----

    # "flags" stores one record for each flag on each bug/attachment.
    flags => {
        FIELDS => [
            id                => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                                  PRIMARYKEY => 1},
            type_id           => {TYPE => 'INT2', NOTNULL => 1},
            status            => {TYPE => 'char(1)', NOTNULL => 1},
            bug_id            => {TYPE => 'INT3', NOTNULL => 1},
            attach_id         => {TYPE => 'INT3'},
            creation_date     => {TYPE => 'DATETIME', NOTNULL => 1},
            modification_date => {TYPE => 'DATETIME'},
            setter_id         => {TYPE => 'INT3'},
            requestee_id      => {TYPE => 'INT3'},
        ],
        INDEXES => [
            flags_bug_id_idx       => [qw(bug_id attach_id)],
            flags_setter_id_idx    => ['setter_id'],
            flags_requestee_id_idx => ['requestee_id'],
            flags_type_id_idx      => ['type_id'],
        ],
    },

    # "flagtypes" defines the types of flags that can be set.
    flagtypes => {
        FIELDS => [
            id               => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                                 PRIMARYKEY => 1},
            name             => {TYPE => 'varchar(50)', NOTNULL => 1},
            description      => {TYPE => 'TEXT'},
            cc_list          => {TYPE => 'varchar(200)'},
            target_type      => {TYPE => 'char(1)', NOTNULL => 1,
                                 DEFAULT => "'b'"},
            is_active        => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                 DEFAULT => 'TRUE'},
            is_requestable   => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                 DEFAULT => 'FALSE'},
            is_requesteeble  => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                 DEFAULT => 'FALSE'},
            is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                 DEFAULT => 'FALSE'},
            sortkey          => {TYPE => 'INT2', NOTNULL => 1,
                                 DEFAULT => '0'},
            grant_group_id   => {TYPE => 'INT3'},
            request_group_id => {TYPE => 'INT3'},
        ],
    },

    # "flaginclusions" and "flagexclusions" specify the products/components
    #     a bug/attachment must belong to in order for flags of a given type
    #     to be set for them.
    flaginclusions => {
        FIELDS => [
            type_id      => {TYPE => 'INT2', NOTNULL => 1},
            product_id   => {TYPE => 'INT2'},
            component_id => {TYPE => 'INT2'},
        ],
        INDEXES => [
            flaginclusions_type_id_idx =>
                [qw(type_id product_id component_id)],
        ],
    },

    flagexclusions => {
        FIELDS => [
            type_id      => {TYPE => 'INT2', NOTNULL => 1},
            product_id   => {TYPE => 'INT2'},
            component_id => {TYPE => 'INT2'},
        ],
        INDEXES => [
            flagexclusions_type_id_idx =>
                [qw(type_id product_id component_id)],
        ],
    },

    # General Field Information
    # -------------------------

    fielddefs => {
        FIELDS => [
            id          => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                            PRIMARYKEY => 1},
            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
            type        => {TYPE => 'INT2', NOTNULL => 1,
                            DEFAULT => FIELD_TYPE_UNKNOWN},
            custom      => {TYPE => 'BOOLEAN', NOTNULL => 1,
                            DEFAULT => 'FALSE'},
            description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            mailhead    => {TYPE => 'BOOLEAN', NOTNULL => 1,
                            DEFAULT => 'FALSE'},
            sortkey     => {TYPE => 'INT2', NOTNULL => 1},
            obsolete    => {TYPE => 'BOOLEAN', NOTNULL => 1,
                            DEFAULT => 'FALSE'},
            enter_bug   => {TYPE => 'BOOLEAN', NOTNULL => 1,
                            DEFAULT => 'FALSE'},
        ],
        INDEXES => [
            fielddefs_name_idx    => {FIELDS => ['name'],
                                      TYPE => 'UNIQUE'},
            fielddefs_sortkey_idx => ['sortkey'],
        ],
    },

    # Per-product Field Values
    # ------------------------

    versions => {
        FIELDS => [
            value      =>  {TYPE => 'varchar(64)', NOTNULL => 1},
            product_id =>  {TYPE => 'INT2', NOTNULL => 1},
        ],
        INDEXES => [
            versions_product_id_idx => {FIELDS => [qw(product_id value)],
                                        TYPE => 'UNIQUE'},
        ],
    },

    milestones => {
        FIELDS => [
            product_id => {TYPE => 'INT2', NOTNULL => 1},
            value      => {TYPE => 'varchar(20)', NOTNULL => 1},
            sortkey    => {TYPE => 'INT2', NOTNULL => 1,
                           DEFAULT => 0},
        ],
        INDEXES => [
            milestones_product_id_idx => {FIELDS => [qw(product_id value)],
                                          TYPE => 'UNIQUE'},
        ],
    },

    # Global Field Values
    # -------------------

    bug_status => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            bug_status_value_idx  => {FIELDS => ['value'],
                                       TYPE => 'UNIQUE'},
            bug_status_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    resolution => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            resolution_value_idx   => {FIELDS => ['value'],
                                       TYPE => 'UNIQUE'},
            resolution_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    bug_severity => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1, 
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            bug_severity_value_idx   => {FIELDS => ['value'],
                                         TYPE => 'UNIQUE'},
            bug_severity_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    priority => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            priority_value_idx   => {FIELDS => ['value'],
                                     TYPE => 'UNIQUE'},
            priority_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    rep_platform => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            rep_platform_value_idx   => {FIELDS => ['value'],
                                         TYPE => 'UNIQUE'},
            rep_platform_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    op_sys => {
        FIELDS => [
            id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            value    => {TYPE => 'varchar(64)', NOTNULL => 1},
            sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
            isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, 
                         DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            op_sys_value_idx   => {FIELDS => ['value'],
                                   TYPE => 'UNIQUE'},
            op_sys_sortkey_idx => ['sortkey', 'value'],
        ],
    },

    # USER INFO
    # ---------

    # General User Information
    # ------------------------

    profiles => {
        FIELDS => [
            userid         => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                               PRIMARYKEY => 1},
            login_name     => {TYPE => 'varchar(255)', NOTNULL => 1},
            cryptpassword  => {TYPE => 'varchar(128)'},
            realname       => {TYPE => 'varchar(255)', NOTNULL => 1,
                               DEFAULT => "''"},
            disabledtext   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
                               DEFAULT => "''"},
            disable_mail   => {TYPE => 'BOOLEAN', NOTNULL => 1,
                               DEFAULT => 'FALSE'},
            mybugslink     => {TYPE => 'BOOLEAN', NOTNULL => 1,
                               DEFAULT => 'TRUE'},
            extern_id      => {TYPE => 'varchar(64)'},
        ],
        INDEXES => [
            profiles_login_name_idx => {FIELDS => ['login_name'],
                                        TYPE => 'UNIQUE'},
        ],
    },

    profiles_activity => {
        FIELDS => [
            userid        => {TYPE => 'INT3', NOTNULL => 1},
            who           => {TYPE => 'INT3', NOTNULL => 1},
            profiles_when => {TYPE => 'DATETIME', NOTNULL => 1},
            fieldid       => {TYPE => 'INT3', NOTNULL => 1},
            oldvalue      => {TYPE => 'TINYTEXT'},
            newvalue      => {TYPE => 'TINYTEXT'},
        ],
        INDEXES => [
            profiles_activity_userid_idx  => ['userid'],
            profiles_activity_profiles_when_idx => ['profiles_when'],
            profiles_activity_fieldid_idx => ['fieldid'],
        ],
    },

    email_setting => {
        FIELDS => [
            user_id      => {TYPE => 'INT3', NOTNULL => 1},
            relationship => {TYPE => 'INT1', NOTNULL => 1},
            event        => {TYPE => 'INT1', NOTNULL => 1},
        ],
        INDEXES => [
            email_setting_user_id_idx  =>
                                    {FIELDS => [qw(user_id relationship event)],
                                     TYPE => 'UNIQUE'},
        ],
    },

    watch => {
        FIELDS => [
            watcher => {TYPE => 'INT3', NOTNULL => 1},
            watched => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            watch_watcher_idx => {FIELDS => [qw(watcher watched)],
                                  TYPE => 'UNIQUE'},
            watch_watched_idx => ['watched'],
        ],
    },

    namedqueries => {
        FIELDS => [
            id           => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                             PRIMARYKEY => 1},
            userid       => {TYPE => 'INT3', NOTNULL => 1},
            name         => {TYPE => 'varchar(64)', NOTNULL => 1},
            query        => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            query_type   => {TYPE => 'BOOLEAN', NOTNULL => 1},
        ],
        INDEXES => [
            namedqueries_userid_idx => {FIELDS => [qw(userid name)],
                                        TYPE => 'UNIQUE'},
        ],
    },

    namedqueries_link_in_footer => {
        FIELDS => [
            namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
            user_id       => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)],
                                                   TYPE => 'UNIQUE'},
            namedqueries_link_in_footer_userid_idx => ['user_id'],
        ],
    },

    component_cc => {

        FIELDS => [
            user_id      => {TYPE => 'INT3', NOTNULL => 1},
            component_id => {TYPE => 'INT2', NOTNULL => 1},
        ],
        INDEXES => [
            component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)],
                                         TYPE => 'UNIQUE'},
        ],
    },

    # Authentication
    # --------------

    logincookies => {
        FIELDS => [
            cookie   => {TYPE => 'varchar(16)', NOTNULL => 1,
                         PRIMARYKEY => 1},
            userid   => {TYPE => 'INT3', NOTNULL => 1},
            ipaddr   => {TYPE => 'varchar(40)', NOTNULL => 1},
            lastused => {TYPE => 'DATETIME', NOTNULL => 1},
        ],
        INDEXES => [
            logincookies_lastused_idx => ['lastused'],
        ],
    },

    # "tokens" stores the tokens users receive when a password or email
    #     change is requested.  Tokens provide an extra measure of security
    #     for these changes.
    tokens => {
        FIELDS => [
            userid    => {TYPE => 'INT3'},
            issuedate => {TYPE => 'DATETIME', NOTNULL => 1} ,
            token     => {TYPE => 'varchar(16)', NOTNULL => 1,
                          PRIMARYKEY => 1},
            tokentype => {TYPE => 'varchar(8)', NOTNULL => 1} ,
            eventdata => {TYPE => 'TINYTEXT'},
        ],
        INDEXES => [
            tokens_userid_idx => ['userid'],
        ],
    },

    # GROUPS
    # ------

    groups => {
        FIELDS => [
            id           => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                             PRIMARYKEY => 1},
            name         => {TYPE => 'varchar(255)', NOTNULL => 1},
            description  => {TYPE => 'TEXT', NOTNULL => 1},
            isbuggroup   => {TYPE => 'BOOLEAN', NOTNULL => 1},
            userregexp   => {TYPE => 'TINYTEXT', NOTNULL => 1,
                             DEFAULT => "''"},
            isactive     => {TYPE => 'BOOLEAN', NOTNULL => 1,
                             DEFAULT => 'TRUE'},
        ],
        INDEXES => [
            groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},
        ],
    },

    group_control_map => {
        FIELDS => [
            group_id      => {TYPE => 'INT3', NOTNULL => 1},
            product_id    => {TYPE => 'INT3', NOTNULL => 1},
            entry         => {TYPE => 'BOOLEAN', NOTNULL => 1},
            membercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
            othercontrol  => {TYPE => 'BOOLEAN', NOTNULL => 1},
            canedit       => {TYPE => 'BOOLEAN', NOTNULL => 1},
        ],
        INDEXES => [
            group_control_map_product_id_idx =>
            {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'},
            group_control_map_group_id_idx    => ['group_id'],
        ],
    },

    # "user_group_map" determines the groups that a user belongs to
    # directly or due to regexp and which groups can be blessed by a user.
    #
    # grant_type:
    # if GRANT_DIRECT - record was explicitly granted
    # if GRANT_DERIVED - record was derived from expanding a group hierarchy
    # if GRANT_REGEXP - record was created by evaluating a regexp
    user_group_map => {
        FIELDS => [
            user_id    => {TYPE => 'INT3', NOTNULL => 1},
            group_id   => {TYPE => 'INT3', NOTNULL => 1},
            isbless    => {TYPE => 'BOOLEAN', NOTNULL => 1,
                           DEFAULT => 'FALSE'},
            grant_type => {TYPE => 'INT1', NOTNULL => 1,
                           DEFAULT => GRANT_DIRECT},
        ],
        INDEXES => [
            user_group_map_user_id_idx =>
                {FIELDS => [qw(user_id group_id grant_type isbless)],
                 TYPE => 'UNIQUE'},
        ],
    },

    # This table determines which groups are made a member of another
    # group, given the ability to bless another group, or given
    # visibility to another groups existence and membership
    # grant_type:
    # if GROUP_MEMBERSHIP - member groups are made members of grantor
    # if GROUP_BLESS - member groups may grant membership in grantor
    # if GROUP_VISIBLE - member groups may see grantor group
    group_group_map => {
        FIELDS => [
            member_id  => {TYPE => 'INT3', NOTNULL => 1},
            grantor_id => {TYPE => 'INT3', NOTNULL => 1},
            grant_type => {TYPE => 'INT1', NOTNULL => 1,
                           DEFAULT => GROUP_MEMBERSHIP},
        ],
        INDEXES => [
            group_group_map_member_id_idx =>
                {FIELDS => [qw(member_id grantor_id grant_type)],
                 TYPE => 'UNIQUE'},
        ],
    },

    # This table determines which groups a user must be a member of
    # in order to see a bug.
    bug_group_map => {
        FIELDS => [
            bug_id   => {TYPE => 'INT3', NOTNULL => 1},
            group_id => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            bug_group_map_bug_id_idx   =>
                {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'},
            bug_group_map_group_id_idx => ['group_id'],
        ],
    },

    # This table determines which groups a user must be a member of
    # in order to see a named query somebody else shares.
    namedquery_group_map => {
        FIELDS => [
            namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
            group_id      => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            namedquery_group_map_namedquery_id_idx   =>
                {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'},
            namedquery_group_map_group_id_idx => ['group_id'],
        ],
    },

    category_group_map => {
        FIELDS => [
            category_id => {TYPE => 'INT2', NOTNULL => 1},
            group_id    => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            category_group_map_category_id_idx =>
                {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'},
        ],
    },


    # PRODUCTS
    # --------

    classifications => {
        FIELDS => [
            id          => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                            PRIMARYKEY => 1},
            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
            description => {TYPE => 'MEDIUMTEXT'},
            sortkey     => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'},
        ],
        INDEXES => [
            classifications_name_idx => {FIELDS => ['name'],
                                           TYPE => 'UNIQUE'},
        ],
    },

    products => {
        FIELDS => [
            id                => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                                  PRIMARYKEY => 1},
            name              => {TYPE => 'varchar(64)', NOTNULL => 1},
            classification_id => {TYPE => 'INT2', NOTNULL => 1,
                                  DEFAULT => '1'},
            description       => {TYPE => 'MEDIUMTEXT'},
            milestoneurl      => {TYPE => 'TINYTEXT', NOTNULL => 1,
                                  DEFAULT => "''"},
            disallownew       => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                  DEFAULT => 0},
            votesperuser      => {TYPE => 'INT2', NOTNULL => 1,
                                  DEFAULT => 0},
            maxvotesperbug    => {TYPE => 'INT2', NOTNULL => 1,
                                  DEFAULT => '10000'},
            votestoconfirm    => {TYPE => 'INT2', NOTNULL => 1,
                                  DEFAULT => 0},
            defaultmilestone  => {TYPE => 'varchar(20)',
                                  NOTNULL => 1, DEFAULT => "'---'"},
        ],
        INDEXES => [
            products_name_idx   => {FIELDS => ['name'],
                                    TYPE => 'UNIQUE'},
        ],
    },

    components => {
        FIELDS => [
            id               => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                                 PRIMARYKEY => 1},
            name             => {TYPE => 'varchar(64)', NOTNULL => 1},
            product_id       => {TYPE => 'INT2', NOTNULL => 1},
            initialowner     => {TYPE => 'INT3', NOTNULL => 1},
            initialqacontact => {TYPE => 'INT3'},
            description      => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
        ],
        INDEXES => [
            components_product_id_idx => {FIELDS => [qw(product_id name)],
                                          TYPE => 'UNIQUE'},
            components_name_idx   => ['name'],
        ],
    },

    # CHARTS
    # ------

    series => {
        FIELDS => [
            series_id   => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                            PRIMARYKEY => 1},
            creator     => {TYPE => 'INT3'},
            category    => {TYPE => 'INT2', NOTNULL => 1},
            subcategory => {TYPE => 'INT2', NOTNULL => 1},
            name        => {TYPE => 'varchar(64)', NOTNULL => 1},
            frequency   => {TYPE => 'INT2', NOTNULL => 1},
            last_viewed => {TYPE => 'DATETIME'},
            query       => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
            is_public   => {TYPE => 'BOOLEAN', NOTNULL => 1,
                            DEFAULT => 'FALSE'},
        ],
        INDEXES => [
            series_creator_idx  =>
                {FIELDS => [qw(creator category subcategory name)],
                 TYPE => 'UNIQUE'},
        ],
    },

    series_data => {
        FIELDS => [
            series_id    => {TYPE => 'INT3', NOTNULL => 1},
            series_date  => {TYPE => 'DATETIME', NOTNULL => 1},
            series_value => {TYPE => 'INT3', NOTNULL => 1},
        ],
        INDEXES => [
            series_data_series_id_idx =>
                {FIELDS => [qw(series_id series_date)],
                 TYPE => 'UNIQUE'},
        ],
    },

    series_categories => {
        FIELDS => [
            id   => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                     PRIMARYKEY => 1},
            name => {TYPE => 'varchar(64)', NOTNULL => 1},
        ],
        INDEXES => [
            series_categories_name_idx => {FIELDS => ['name'],
                                           TYPE => 'UNIQUE'},
        ],
    },

    # WHINE SYSTEM
    # ------------

    whine_queries => {
        FIELDS => [
            id            => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
                              NOTNULL => 1},
            eventid       => {TYPE => 'INT3', NOTNULL => 1},
            query_name    => {TYPE => 'varchar(64)', NOTNULL => 1,
                              DEFAULT => "''"},
            sortkey       => {TYPE => 'INT2', NOTNULL => 1,
                              DEFAULT => '0'},
            onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1,
                              DEFAULT => 'FALSE'},
            title         => {TYPE => 'varchar(128)', NOTNULL => 1,
                              DEFAULT => "''"},
        ],
        INDEXES => [
            whine_queries_eventid_idx => ['eventid'],
        ],
    },

    whine_schedules => {
        FIELDS => [
            id          => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
                            NOTNULL => 1},
            eventid     => {TYPE => 'INT3', NOTNULL => 1},
            run_day     => {TYPE => 'varchar(32)'},
            run_time    => {TYPE => 'varchar(32)'},
            run_next    => {TYPE => 'DATETIME'},
            mailto      => {TYPE => 'INT3', NOTNULL => 1},
            mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'},
        ],
        INDEXES => [
            whine_schedules_run_next_idx => ['run_next'],
            whine_schedules_eventid_idx  => ['eventid'],
        ],
    },

    whine_events => {
        FIELDS => [
            id           => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1,
                             NOTNULL => 1},
            owner_userid => {TYPE => 'INT3', NOTNULL => 1},
            subject      => {TYPE => 'varchar(128)'},
            body         => {TYPE => 'MEDIUMTEXT'},
        ],
    },

    # QUIPS
    # -----

    quips => {
        FIELDS => [
            quipid   => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
                         PRIMARYKEY => 1},
            userid   => {TYPE => 'INT3'},
            quip     => {TYPE => 'TEXT', NOTNULL => 1},
            approved => {TYPE => 'BOOLEAN', NOTNULL => 1,
                         DEFAULT => 'TRUE'},
        ],
    },

    # SETTINGS
    # --------
    # setting          - each global setting will have exactly one entry
    #                    in this table.
    # setting_value    - stores the list of acceptable values for each
    #                    setting, and a sort index that controls the order
    #                    in which the values are displayed.
    # profile_setting  - If a user has chosen to use a value other than the
    #                    global default for a given setting, it will be
    #                    stored in this table. Note: even if a setting is
    #                    later changed so is_enabled = false, the stored
    #                    value will remain in case it is ever enabled again.
    #
    setting => {
        FIELDS => [
            name          => {TYPE => 'varchar(32)', NOTNULL => 1,
                              PRIMARYKEY => 1}, 
            default_value => {TYPE => 'varchar(32)', NOTNULL => 1},
            is_enabled    => {TYPE => 'BOOLEAN', NOTNULL => 1,
                              DEFAULT => 'TRUE'},
            subclass      => {TYPE => 'varchar(32)'},
        ],
    },

    setting_value => {
        FIELDS => [
            name        => {TYPE => 'varchar(32)', NOTNULL => 1},
            value       => {TYPE => 'varchar(32)', NOTNULL => 1},
            sortindex   => {TYPE => 'INT2', NOTNULL => 1},
        ],
        INDEXES => [
            setting_value_nv_unique_idx  => {FIELDS => [qw(name value)],
                                             TYPE => 'UNIQUE'},
            setting_value_ns_unique_idx  => {FIELDS => [qw(name sortindex)],
                                             TYPE => 'UNIQUE'},
        ],
     },

    profile_setting => {
        FIELDS => [
            user_id       => {TYPE => 'INT3', NOTNULL => 1},
            setting_name  => {TYPE => 'varchar(32)', NOTNULL => 1},
            setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
        ],
        INDEXES => [
            profile_setting_value_unique_idx  => {FIELDS => [qw(user_id setting_name)],
                                                  TYPE => 'UNIQUE'},
        ],
     },

    # SCHEMA STORAGE
    # --------------

    bz_schema => {
        FIELDS => [
            schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1},
            version     => {TYPE => 'decimal(3,2)', NOTNULL => 1},
        ],
    },

};

use constant FIELD_TABLE_SCHEMA => {
    FIELDS => [
        id       => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
                     PRIMARYKEY => 1},
        value    => {TYPE => 'varchar(64)', NOTNULL => 1},
        sortkey  => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
        isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
                     DEFAULT => 'TRUE'},
    ],
    # Note that bz_add_field_table should prepend the table name
    # to these index names.
    INDEXES => [
        value_idx   => {FIELDS => ['value'], TYPE => 'UNIQUE'},
        sortkey_idx => ['sortkey', 'value'],
    ],
};
#--------------------------------------------------------------------------

=head1 METHODS

Note: Methods which can be implemented generically for all DBs are
implemented in this module. If needed, they can be overridden with
DB-specific code in a subclass. Methods which are prefixed with C<_>
are considered protected. Subclasses may override these methods, but
other modules should not invoke these methods directly.

=cut

#--------------------------------------------------------------------------
sub new {

=over

=item C<new>

 Description: Public constructor method used to instantiate objects of this
              class. However, it also can be used as a factory method to
              instantiate database-specific subclasses when an optional
              driver argument is supplied.
 Parameters:  $driver (optional) - Used to specify the type of database.
              This routine C<die>s if no subclass is found for the specified
              driver.
              $schema (optional) - A reference to a hash. Callers external
                  to this package should never use this parameter.
 Returns:     new instance of the Schema class or a database-specific subclass

=cut

    my $this = shift;
    my $class = ref($this) || $this;
    my $driver = shift;

    if ($driver) {
        (my $subclass = $driver) =~ s/^(\S)/\U$1/;
        $class .= '::' . $subclass;
        eval "require $class;";
        die "The $class class could not be found ($subclass " .
            "not supported?): $@" if ($@);
    }
    die "$class is an abstract base class. Instantiate a subclass instead."
      if ($class eq __PACKAGE__);

    my $self = {};
    bless $self, $class;
    $self = $self->_initialize(@_);

    return($self);

} #eosub--new
#--------------------------------------------------------------------------
sub _initialize {

=item C<_initialize>

 Description: Protected method that initializes an object after
              instantiation with the abstract schema. All subclasses should
              override this method. The typical subclass implementation
              should first call the C<_initialize> method of the superclass,
              then do any database-specific initialization (especially
              define the database-specific implementation of the all
              abstract data types), and then call the C<_adjust_schema>
              method.
 Parameters:  $abstract_schema (optional) - A reference to a hash. If 
                  provided, this hash will be used as the internal
                  representation of the abstract schema instead of our
                  default abstract schema. This is intended for internal 
                  use only by deserialize_abstract.
 Returns:     the instance of the Schema class

=cut

    my $self = shift;
    my $abstract_schema = shift;

    $abstract_schema ||= ABSTRACT_SCHEMA;

    # Let extensions add tables, but make sure they can't modify existing 
    # tables. If we don't lock/unlock keys, lock_value complains.
    lock_keys(%$abstract_schema);
    lock_value(%$abstract_schema, $_) foreach (keys %$abstract_schema);
    unlock_keys(%$abstract_schema);
    Bugzilla::Hook::process('db_schema-abstract_schema', 
                            { schema => $abstract_schema });
    unlock_hash(%$abstract_schema);

    $self->{schema} = dclone($abstract_schema);
    # While ABSTRACT_SCHEMA cannot be modified, 
    # $self->{abstract_schema} can be. So, we dclone it to prevent
    # anything from mucking with the constant.
    $self->{abstract_schema} = dclone($abstract_schema);

    return $self;

} #eosub--_initialize
#--------------------------------------------------------------------------
sub _adjust_schema {

=item C<_adjust_schema>

 Description: Protected method that alters the abstract schema at
              instantiation-time to be database-specific. It is a generic
              enough routine that it can be defined here in the base class.
              It takes the abstract schema and replaces the abstract data
              types with database-specific data types.
 Parameters:  none
 Returns:     the instance of the Schema class

=cut

    my $self = shift;

    # The _initialize method has already set up the db_specific hash with
    # the information on how to implement the abstract data types for the
    # instantiated DBMS-specific subclass.
    my $db_specific = $self->{db_specific};

    # Loop over each table in the abstract database schema.
    foreach my $table (keys %{ $self->{schema} }) {
        my %fields = (@{ $self->{schema}{$table}{FIELDS} });
        # Loop over the field definitions in each table.
        foreach my $field_def (values %fields) {
            # If the field type is an abstract data type defined in the
            # $db_specific hash, replace it with the DBMS-specific data type
            # that implements it.
            if (exists($db_specific->{$field_def->{TYPE}})) {
                $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}};
            }
            # Replace abstract default values (such as 'TRUE' and 'FALSE')
            # with their database-specific implementations.
            if (exists($field_def->{DEFAULT})
                && exists($db_specific->{$field_def->{DEFAULT}})) {
                $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}};
            }
        }
    }

    return $self;

} #eosub--_adjust_schema
#--------------------------------------------------------------------------
sub get_type_ddl {

=item C<get_type_ddl>

 Description: Public method to convert abstract (database-generic) field
              specifiers to database-specific data types suitable for use
              in a C<CREATE TABLE> or C<ALTER TABLE> SQL statment. If no
              database-specific field type has been defined for the given
              field type, then it will just return the same field type.
 Parameters:  a hash or a reference to a hash of a field containing the
              following keys: C<TYPE> (required), C<NOTNULL> (optional),
              C<DEFAULT> (optional), C<PRIMARYKEY> (optional), C<REFERENCES>
              (optional)
 Returns:     a DDL string suitable for describing a field in a
              C<CREATE TABLE> or C<ALTER TABLE> SQL statement

=cut

    my $self = shift;
    my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ };

    my $type = $finfo->{TYPE};
    die "A valid TYPE was not specified for this column." unless ($type);

    my $default = $finfo->{DEFAULT};
    # Replace any abstract default value (such as 'TRUE' or 'FALSE')
    # with its database-specific implementation.
    if ( defined $default && exists($self->{db_specific}->{$default}) ) {
        $default = $self->{db_specific}->{$default};
    }

    my $fkref = $self->{enable_references} ? $finfo->{REFERENCES} : undef;
    my $type_ddl = $self->{db_specific}{$type} || $type;
    # DEFAULT attribute must appear before any column constraints
    # (e.g., NOT NULL), for Oracle
    $type_ddl .= " DEFAULT $default" if (defined($default));
    $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL});
    $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY});
    $type_ddl .= "\n\t\t\t\tREFERENCES $fkref" if $fkref;

    return($type_ddl);

} #eosub--get_type_ddl
#--------------------------------------------------------------------------
sub get_column {
=item C<get_column($table, $column)>

 Description: Public method to get the abstract definition of a column.
 Parameters:  $table - the table name
              $column - a column in the table
 Returns:     a hashref containing information about the column, including its
              type (C<TYPE>), whether or not it can be null (C<NOTNULL>),
              its default value if it has one (C<DEFAULT), etc.
              Returns undef if the table or column does not exist.

=cut

    my($self, $table, $column) = @_;

    # Prevent a possible dereferencing of an undef hash, if the
    # table doesn't exist.
    if (exists $self->{schema}->{$table}) {
        my %fields = (@{ $self->{schema}{$table}{FIELDS} });
        return $fields{$column};
    }
    return undef;
} #eosub--get_column

sub get_table_list {

=item C<get_table_list>

 Description: Public method for discovering what tables should exist in the
              Bugzilla database.
 Parameters:  none
 Returns:     an array of table names

=cut

    my $self = shift;

    return(sort(keys %{ $self->{schema} }));

} #eosub--get_table_list
#--------------------------------------------------------------------------
sub get_table_columns {

=item C<get_table_columns>

 Description: Public method for discovering what columns are in a given
              table in the Bugzilla database.
 Parameters:  $table - the table name
 Returns:     array of column names

=cut

    my($self, $table) = @_;
    my @ddl = ();

    my $thash = $self->{schema}{$table};
    die "Table $table does not exist in the database schema."
        unless (ref($thash));

    my @columns = ();
    my @fields = @{ $thash->{FIELDS} };
    while (@fields) {
        push(@columns, shift(@fields));
        shift(@fields);
    }

    return @columns;

} #eosub--get_table_columns
#--------------------------------------------------------------------------
sub get_table_ddl {

=item C<get_table_ddl>

 Description: Public method to generate the SQL statements needed to create
              the a given table and its indexes in the Bugzilla database.
              Subclasses may override or extend this method, if needed, but
              subclasses probably should override C<_get_create_table_ddl>
              or C<_get_create_index_ddl> instead.
 Parameters:  $table - the table name
 Returns:     an array of strings containing SQL statements

=cut

    my($self, $table) = @_;
    my @ddl = ();

    die "Table $table does not exist in the database schema."
        unless (ref($self->{schema}{$table}));

    my $create_table = $self->_get_create_table_ddl($table);
    push(@ddl, $create_table) if $create_table;

    my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] };
    while (@indexes) {
        my $index_name = shift(@indexes);
        my $index_info = shift(@indexes);
        my $index_sql  = $self->get_add_index_ddl($table, $index_name, 
                                                  $index_info);
        push(@ddl, $index_sql) if $index_sql;
    }

    push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} })
      if (ref($self->{schema}{$table}{DB_EXTRAS}));

    return @ddl;

} #eosub--get_table_ddl
#--------------------------------------------------------------------------
sub _get_create_table_ddl {

=item C<_get_create_table_ddl>

 Description: Protected method to generate the "create table" SQL statement
              for a given table.
 Parameters:  $table - the table name
 Returns:     a string containing the DDL statement for the specified table

=cut

    my($self, $table) = @_;

    my $thash = $self->{schema}{$table};
    die "Table $table does not exist in the database schema."
        unless (ref($thash));

    my $create_table = "CREATE TABLE $table \(\n";

    my @fields = @{ $thash->{FIELDS} };
    while (@fields) {
        my $field = shift(@fields);
        my $finfo = shift(@fields);
        $create_table .= "\t$field\t" . $self->get_type_ddl($finfo);
        $create_table .= "," if (@fields);
        $create_table .= "\n";
    }

    $create_table .= "\)";

    return($create_table)

} #eosub--_get_create_table_ddl
#--------------------------------------------------------------------------
sub _get_create_index_ddl {

=item C<_get_create_index_ddl>

 Description: Protected method to generate a "create index" SQL statement
              for a given table and index.
 Parameters:  $table_name - the name of the table
              $index_name - the name of the index
              $index_fields - a reference to an array of field names
              $index_type (optional) - specify type of index (e.g., UNIQUE)
 Returns:     a string containing the DDL statement

=cut

    my($self, $table_name, $index_name, $index_fields, $index_type) = @_;

    my $sql = "CREATE ";
    $sql .= "$index_type " if ($index_type eq 'UNIQUE');
    $sql .= "INDEX $index_name ON $table_name \(" .
      join(", ", @$index_fields) . "\)";

    return($sql);

} #eosub--_get_create_index_ddl
#--------------------------------------------------------------------------

sub get_add_column_ddl {

=item C<get_add_column_ddl($table, $column, \%definition, $init_value)>

 Description: Generate SQL to add a column to a table.
 Params:      $table - The table containing the column.
              $column - The name of the column being added.
              \%definition - The new definition for the column,
                  in standard C<ABSTRACT_SCHEMA> format.
              $init_value - (optional) An initial value to set 
                            the column to. Should already be SQL-quoted
                            if necessary.
 Returns:     An array of SQL statements.

=cut

    my ($self, $table, $column, $definition, $init_value) = @_;
    my @statements;
    push(@statements, "ALTER TABLE $table ADD COLUMN $column " .
        $self->get_type_ddl($definition));

    # XXX - Note that although this works for MySQL, most databases will fail
    # before this point, if we haven't set a default.
    (push(@statements, "UPDATE $table SET $column = $init_value"))
        if defined $init_value;

    return (@statements);
}

sub get_add_index_ddl {

=item C<get_add_index_ddl>

 Description: Gets SQL for creating an index.
              NOTE: Subclasses should not override this function. Instead,
              if they need to specify a custom CREATE INDEX statement, 
              they should override C<_get_create_index_ddl>
 Params:      $table - The name of the table the index will be on.
              $name  - The name of the new index.
              $definition - An index definition. Either a hashref 
                            with FIELDS and TYPE or an arrayref 
                            containing a list of columns.
 Returns:     An array of SQL statements that will create the 
              requested index.

=cut

    my ($self, $table, $name, $definition) = @_;

    my ($index_fields, $index_type);
    # Index defs can be arrays or hashes
    if (ref($definition) eq 'HASH') {
        $index_fields = $definition->{FIELDS};
        $index_type = $definition->{TYPE};
    } else {
        $index_fields = $definition;
        $index_type = '';
    }
    
    return $self->_get_create_index_ddl($table, $name, $index_fields, 
                                        $index_type);
}

sub get_alter_column_ddl {

=item C<get_alter_column_ddl($table, $column, \%definition)>

 Description: Generate SQL to alter a column in a table.
              The column that you are altering must exist,
              and the table that it lives in must exist.
 Params:      $table - The table containing the column.
              $column - The name of the column being changed.
              \%definition - The new definition for the column,
                  in standard C<ABSTRACT_SCHEMA> format.
              $set_nulls_to - A value to set NULL values to, if
                  your new definition is NOT NULL and contains
                  no DEFAULT, and when there is a possibility
                  that the column could contain NULLs. $set_nulls_to
                  should be already SQL-quoted if necessary.
 Returns:     An array of SQL statements.

=cut

    my ($self, $table, $column, $new_def, $set_nulls_to) = @_;

    my @statements;
    my $old_def = $self->get_column_abstract($table, $column);
    my $specific = $self->{db_specific};

    # If the types have changed, we have to deal with that.
    if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
        push(@statements, $self->_get_alter_type_sql($table, $column, 
                                                     $new_def, $old_def));
    }

    my $default = $new_def->{DEFAULT};
    my $default_old = $old_def->{DEFAULT};
    # This first condition prevents "uninitialized value" errors.
    if (!defined $default && !defined $default_old) {
        # Do Nothing
    }
    # If we went from having a default to not having one
    elsif (!defined $default && defined $default_old) {
        push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
                        . " DROP DEFAULT");
    }
    # If we went from no default to a default, or we changed the default.
    elsif ( (defined $default && !defined $default_old) || 
            ($default ne $default_old) ) 
    {
        $default = $specific->{$default} if exists $specific->{$default};
        push(@statements, "ALTER TABLE $table ALTER COLUMN $column "
                         . " SET DEFAULT $default");
    }

    # If we went from NULL to NOT NULL.
    if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
        my $setdefault;
        # Handle any fields that were NULL before, if we have a default,
        $setdefault = $new_def->{DEFAULT} if exists $new_def->{DEFAULT};
        # But if we have a set_nulls_to, that overrides the DEFAULT 
        # (although nobody would usually specify both a default and 
        # a set_nulls_to.)
        $setdefault = $set_nulls_to if defined $set_nulls_to;
        if (defined $setdefault) {
            push(@statements, "UPDATE $table SET $column = $setdefault"
                            . "  WHERE $column IS NULL");
        }
        push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
                        . " SET NOT NULL");
    }
    # If we went from NOT NULL to NULL
    elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
        push(@statements, "ALTER TABLE $table ALTER COLUMN $column"
                        . " DROP NOT NULL");
    }

    # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
    if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
        push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
    }
    # If we went from being a PK to not being a PK
    elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) {
        push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
    }

    return @statements;
}

sub get_drop_index_ddl {

=item C<get_drop_index_ddl($table, $name)>

 Description: Generates SQL statements to drop an index.
 Params:      $table - The table the index is on.
              $name  - The name of the index being dropped.
 Returns:     An array of SQL statements.

=cut

    my ($self, $table, $name) = @_;

    # Although ANSI SQL-92 doesn't specify a method of dropping an index,
    # many DBs support this syntax.
    return ("DROP INDEX $name");
}