1999-04-08 15:29:08 +02:00
|
|
|
<Chapter Id="xoper">
|
|
|
|
<Title>Extending <Acronym>SQL</Acronym>: Operators</Title>
|
|
|
|
|
|
|
|
<Para>
|
|
|
|
<ProductName>Postgres</ProductName> supports left unary,
|
|
|
|
right unary and binary
|
1999-05-21 02:38:33 +02:00
|
|
|
operators. Operators can be overloaded; that is,
|
|
|
|
the same operator name can be used for different operators
|
|
|
|
that have different numbers and types of arguments. If
|
1999-04-08 15:29:08 +02:00
|
|
|
there is an ambiguous situation and the system cannot
|
|
|
|
determine the correct operator to use, it will return
|
1999-05-21 02:38:33 +02:00
|
|
|
an error. You may have to typecast the left and/or
|
1999-04-08 15:29:08 +02:00
|
|
|
right operands to help it understand which operator you
|
|
|
|
meant to use.
|
1999-05-21 02:38:33 +02:00
|
|
|
</Para>
|
|
|
|
|
|
|
|
<Para>
|
|
|
|
Every operator is "syntactic sugar" for a call to an
|
|
|
|
underlying function that does the real work; so you must
|
|
|
|
first create the underlying function before you can create
|
|
|
|
the operator. However, an operator is <emphasis>not</emphasis>
|
|
|
|
merely syntactic sugar, because it carries additional information
|
|
|
|
that helps the query planner optimize queries that use the
|
|
|
|
operator. Much of this chapter will be devoted to explaining
|
|
|
|
that additional information.
|
|
|
|
</Para>
|
|
|
|
|
|
|
|
<Para>
|
|
|
|
Here is an example of creating an operator for adding two
|
|
|
|
complex numbers. We assume we've already created the definition
|
|
|
|
of type complex. First we need a function that does the work;
|
|
|
|
then we can define the operator:
|
1999-04-08 15:29:08 +02:00
|
|
|
|
|
|
|
<ProgramListing>
|
|
|
|
CREATE FUNCTION complex_add(complex, complex)
|
|
|
|
RETURNS complex
|
|
|
|
AS '$PWD/obj/complex.so'
|
|
|
|
LANGUAGE 'c';
|
|
|
|
|
|
|
|
CREATE OPERATOR + (
|
|
|
|
leftarg = complex,
|
|
|
|
rightarg = complex,
|
|
|
|
procedure = complex_add,
|
|
|
|
commutator = +
|
|
|
|
);
|
|
|
|
</ProgramListing>
|
|
|
|
</Para>
|
|
|
|
|
|
|
|
<Para>
|
1999-05-21 02:38:33 +02:00
|
|
|
Now we can do:
|
1998-03-01 09:16:16 +01:00
|
|
|
|
1999-04-08 15:29:08 +02:00
|
|
|
<ProgramListing>
|
|
|
|
SELECT (a + b) AS c FROM test_complex;
|
|
|
|
|
|
|
|
+----------------+
|
|
|
|
|c |
|
|
|
|
+----------------+
|
|
|
|
|(5.2,6.05) |
|
|
|
|
+----------------+
|
|
|
|
|(133.42,144.95) |
|
|
|
|
+----------------+
|
|
|
|
</ProgramListing>
|
|
|
|
</Para>
|
|
|
|
|
1999-05-21 02:38:33 +02:00
|
|
|
<Para>
|
|
|
|
We've shown how to create a binary operator here. To
|
|
|
|
create unary operators, just omit one of leftarg (for
|
|
|
|
left unary) or rightarg (for right unary). The procedure
|
|
|
|
clause and the argument clauses are the only required items
|
|
|
|
in CREATE OPERATOR. The COMMUTATOR clause shown in the example
|
|
|
|
is an optional hint to the query optimizer. Further details about
|
|
|
|
COMMUTATOR and other optimizer hints appear below.
|
|
|
|
</Para>
|
|
|
|
|
1999-04-08 15:29:08 +02:00
|
|
|
<sect1>
|
1999-05-21 02:38:33 +02:00
|
|
|
<title>Operator Optimization Information</title>
|
1999-04-08 15:29:08 +02:00
|
|
|
|
|
|
|
<note>
|
|
|
|
<title>Author</title>
|
|
|
|
<para>
|
|
|
|
Written by Tom Lane.
|
|
|
|
</para>
|
|
|
|
</note>
|
|
|
|
|
|
|
|
<para>
|
1999-05-21 02:38:33 +02:00
|
|
|
A <ProductName>Postgres</ProductName> operator definition can include
|
|
|
|
several optional clauses that tell the system useful things about how
|
|
|
|
the operator behaves. These clauses should be provided whenever
|
|
|
|
appropriate, because they can make for considerable speedups in execution
|
|
|
|
of queries that use the operator. But if you provide them, you must be
|
|
|
|
sure that they are right! Incorrect use of an optimization clause can
|
|
|
|
result in backend crashes, subtly wrong output, or other Bad Things.
|
|
|
|
You can always leave out an optimization clause if you are not sure
|
1999-05-26 19:30:30 +02:00
|
|
|
about it; the only consequence is that queries might run slower than
|
1999-05-21 02:38:33 +02:00
|
|
|
they need to.
|
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
Additional optimization clauses might be added in future versions of
|
|
|
|
<ProductName>Postgres</ProductName>. The ones described here are all
|
|
|
|
the ones that release 6.5 understands.
|
|
|
|
</para>
|
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<sect2>
|
|
|
|
<title>COMMUTATOR</title>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The COMMUTATOR clause, if provided, names an operator that is the
|
|
|
|
commutator of the operator being defined. We say that operator A is the
|
|
|
|
commutator of operator B if (x A y) equals (y B x) for all possible input
|
|
|
|
values x,y. Notice that B is also the commutator of A. For example,
|
|
|
|
operators '<' and '>' for a particular datatype are usually each others'
|
|
|
|
commutators, and operator '+' is usually commutative with itself.
|
|
|
|
But operator '-' is usually not commutative with anything.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The left argument type of a commuted operator is the same as the
|
|
|
|
right argument type of its commutator, and vice versa. So the name of
|
|
|
|
the commutator operator is all that <ProductName>Postgres</ProductName>
|
|
|
|
needs to be given to look up the commutator, and that's all that need
|
|
|
|
be provided in the COMMUTATOR clause.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
When you are defining a self-commutative operator, you just do it.
|
|
|
|
When you are defining a pair of commutative operators, things are
|
|
|
|
a little trickier: how can the first one to be defined refer to the
|
|
|
|
other one, which you haven't defined yet? There are two solutions
|
|
|
|
to this problem:
|
|
|
|
|
|
|
|
<itemizedlist>
|
|
|
|
<listitem>
|
|
|
|
<para>
|
|
|
|
One way is to omit the COMMUTATOR clause in the first operator that
|
|
|
|
you define, and then provide one in the second operator's definition.
|
|
|
|
Since <ProductName>Postgres</ProductName> knows that commutative
|
|
|
|
operators come in pairs, when it sees the second definition it will
|
|
|
|
automatically go back and fill in the missing COMMUTATOR clause in
|
|
|
|
the first definition.
|
|
|
|
</para>
|
|
|
|
</listitem>
|
|
|
|
|
|
|
|
<listitem>
|
|
|
|
<para>
|
|
|
|
The other, more straightforward way is just to include COMMUTATOR clauses
|
|
|
|
in both definitions. When <ProductName>Postgres</ProductName> processes
|
|
|
|
the first definition and realizes that COMMUTATOR refers to a non-existent
|
|
|
|
operator, the system will make a dummy entry for that operator in the
|
|
|
|
system's pg_operator table. This dummy entry will have valid data only
|
|
|
|
for the operator name, left and right argument types, and result type,
|
|
|
|
since that's all that <ProductName>Postgres</ProductName> can deduce
|
|
|
|
at this point. The first operator's catalog entry will link to this
|
|
|
|
dummy entry. Later, when you define the second operator, the system
|
|
|
|
updates the dummy entry with the additional information from the second
|
|
|
|
definition. If you try to use the dummy operator before it's been filled
|
|
|
|
in, you'll just get an error message. (Note: this procedure did not work
|
|
|
|
reliably in <ProductName>Postgres</ProductName> versions before 6.5,
|
|
|
|
but it is now the recommended way to do things.)
|
|
|
|
</para>
|
|
|
|
</listitem>
|
|
|
|
</itemizedlist>
|
|
|
|
</para>
|
|
|
|
</sect2>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<sect2>
|
|
|
|
<title>NEGATOR</title>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The NEGATOR clause, if provided, names an operator that is the
|
|
|
|
negator of the operator being defined. We say that operator A
|
|
|
|
is the negator of operator B if both return boolean results and
|
|
|
|
(x A y) equals NOT (x B y) for all possible inputs x,y.
|
|
|
|
Notice that B is also the negator of A.
|
|
|
|
For example, '<' and '>=' are a negator pair for most datatypes.
|
|
|
|
An operator can never be validly be its own negator.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
|
|
|
<para>
|
|
|
|
Unlike COMMUTATOR, a pair of unary operators could validly be marked
|
|
|
|
as each others' negators; that would mean (A x) equals NOT (B x)
|
|
|
|
for all x, or the equivalent for right-unary operators.
|
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
An operator's negator must have the same left and/or right argument types
|
|
|
|
as the operator itself, so just as with COMMUTATOR, only the operator
|
|
|
|
name need be given in the NEGATOR clause.
|
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
Providing NEGATOR is very helpful to the query optimizer since
|
|
|
|
it allows expressions like NOT (x = y) to be simplified into
|
1999-05-22 04:27:25 +02:00
|
|
|
x <> y. This comes up more often than you might think, because
|
1999-05-21 02:38:33 +02:00
|
|
|
NOTs can be inserted as a consequence of other rearrangements.
|
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
Pairs of negator operators can be defined using the same methods
|
|
|
|
explained above for commutator pairs.
|
|
|
|
</para>
|
|
|
|
|
|
|
|
</sect2>
|
|
|
|
|
|
|
|
<sect2>
|
|
|
|
<title>RESTRICT</title>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
The RESTRICT clause, if provided, names a restriction selectivity
|
|
|
|
estimation function for the operator (note that this is a function
|
|
|
|
name, not an operator name). RESTRICT clauses only make sense for
|
|
|
|
binary operators that return boolean. The idea behind a restriction
|
|
|
|
selectivity estimator is to guess what fraction of the rows in a
|
|
|
|
table will satisfy a WHERE-clause condition of the form
|
|
|
|
<ProgramListing>
|
|
|
|
field OP constant
|
|
|
|
</ProgramListing>
|
|
|
|
for the current operator and a particular constant value.
|
|
|
|
This assists the optimizer by
|
|
|
|
giving it some idea of how many rows will be eliminated by WHERE
|
|
|
|
clauses that have this form. (What happens if the constant is on
|
|
|
|
the left, you may be wondering? Well, that's one of the things that
|
|
|
|
COMMUTATOR is for...)
|
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
Writing new restriction selectivity estimation functions is far beyond
|
|
|
|
the scope of this chapter, but fortunately you can usually just use
|
|
|
|
one of the system's standard estimators for many of your own operators.
|
|
|
|
These are the standard restriction estimators:
|
|
|
|
<ProgramListing>
|
|
|
|
eqsel for =
|
1999-05-22 04:27:25 +02:00
|
|
|
neqsel for <>
|
|
|
|
intltsel for < or <=
|
|
|
|
intgtsel for > or >=
|
1999-05-21 02:38:33 +02:00
|
|
|
</ProgramListing>
|
|
|
|
It might seem a little odd that these are the categories, but they
|
|
|
|
make sense if you think about it. '=' will typically accept only
|
1999-05-22 04:27:25 +02:00
|
|
|
a small fraction of the rows in a table; '<>' will typically reject
|
|
|
|
only a small fraction. '<' will accept a fraction that depends on
|
1999-05-21 02:38:33 +02:00
|
|
|
where the given constant falls in the range of values for that table
|
|
|
|
column (which, it just so happens, is information collected by
|
|
|
|
VACUUM ANALYZE and made available to the selectivity estimator).
|
1999-05-22 04:27:25 +02:00
|
|
|
'<=' will accept a slightly larger fraction than '<' for the same
|
1999-05-21 02:38:33 +02:00
|
|
|
comparison constant, but they're close enough to not be worth
|
|
|
|
distinguishing, especially since we're not likely to do better than a
|
1999-05-22 04:27:25 +02:00
|
|
|
rough guess anyhow. Similar remarks apply to '>' and '>='.
|
1999-05-21 02:38:33 +02:00
|
|
|
</para>
|
|
|
|
|
|
|
|
<para>
|
|
|
|
You can frequently get away with using either eqsel or neqsel for
|
|
|
|
operators that have very high or very low selectivity, even if they
|
|
|
|
aren't really equality or inequality. For example, the regular expression
|
|
|
|
matching operators (~, ~*, etc) use eqsel on the assumption that they'll
|
|
|
|
usually only match a small fraction of the entries in a table.
|
|
|
|
</para>
|
1999-05-22 04:27:25 +02:00
|
|
|
</sect2>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-22 04:27:25 +02:00
|
|
|
<sect2>
|
|
|
|
<title>JOIN</title>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-22 04:27:25 +02:00
|
|
|
<para>
|
|
|
|
The JOIN clause, if provided, names a join selectivity
|
|
|
|
estimation function for the operator (note that this is a function
|
|
|
|
name, not an operator name). JOIN clauses only make sense for
|
|
|
|
binary operators that return boolean. The idea behind a join
|
|
|
|
selectivity estimator is to guess what fraction of the rows in a
|
|
|
|
pair of tables will satisfy a WHERE-clause condition of the form
|
|
|
|
<ProgramListing>
|
1999-05-21 02:38:33 +02:00
|
|
|
table1.field1 OP table2.field2
|
1999-05-22 04:27:25 +02:00
|
|
|
</ProgramListing>
|
|
|
|
for the current operator. As with the RESTRICT clause, this helps
|
|
|
|
the optimizer very substantially by letting it figure out which
|
|
|
|
of several possible join sequences is likely to take the least work.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-22 04:27:25 +02:00
|
|
|
<para>
|
|
|
|
As before, this chapter will make no attempt to explain how to write
|
|
|
|
a join selectivity estimator function, but will just suggest that
|
|
|
|
you use one of the standard estimators if one is applicable:
|
|
|
|
<ProgramListing>
|
1999-05-21 02:38:33 +02:00
|
|
|
eqjoinsel for =
|
1999-05-22 04:27:25 +02:00
|
|
|
neqjoinsel for <>
|
|
|
|
intltjoinsel for < or <=
|
|
|
|
intgtjoinsel for > or >=
|
|
|
|
</ProgramListing>
|
|
|
|
</para>
|
|
|
|
</sect2>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-22 04:27:25 +02:00
|
|
|
<sect2>
|
|
|
|
<title>HASHES</title>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-22 04:27:25 +02:00
|
|
|
<para>
|
|
|
|
The HASHES clause, if present, tells the system that it is OK to
|
|
|
|
use the hash join method for a join based on this operator. HASHES
|
1999-05-26 19:30:30 +02:00
|
|
|
only makes sense for binary operators that return boolean, and
|
|
|
|
in practice the operator had better be equality for some data type.
|
1999-05-22 04:27:25 +02:00
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The assumption underlying hash join is that the join operator can
|
|
|
|
only return TRUE for pairs of left and right values that hash to the
|
|
|
|
same hash code. If two values get put in different hash buckets, the
|
|
|
|
join will never compare them at all, implicitly assuming that the
|
|
|
|
result of the join operator must be FALSE. So it never makes sense
|
|
|
|
to specify HASHES for operators that do not represent equality.
|
|
|
|
</para>
|
1999-04-08 15:29:08 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
In fact, logical equality is not good enough either; the operator
|
|
|
|
had better represent pure bitwise equality, because the hash function
|
|
|
|
will be computed on the memory representation of the values regardless
|
|
|
|
of what the bits mean. For example, equality of
|
|
|
|
time intervals is not bitwise equality; the interval equality operator
|
|
|
|
considers two time intervals equal if they have the same
|
|
|
|
duration, whether or not their endpoints are identical. What this means
|
|
|
|
is that a join using "=" between interval fields would yield different
|
|
|
|
results if implemented as a hash join than if implemented another way,
|
|
|
|
because a large fraction of the pairs that should match will hash to
|
|
|
|
different values and will never be compared by the hash join. But
|
|
|
|
if the optimizer chose to use a different kind of join, all the pairs
|
|
|
|
that the equality operator says are equal will be found.
|
|
|
|
We don't want that kind of inconsistency, so we don't mark interval
|
|
|
|
equality as hashable.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
There are also machine-dependent ways in which a hash join might fail
|
|
|
|
to do the right thing. For example, if your datatype
|
|
|
|
is a structure in which there may be uninteresting pad bits, it's unsafe
|
|
|
|
to mark the equality operator HASHES. (Unless, perhaps, you write
|
|
|
|
your other operators to ensure that the unused bits are always zero.)
|
|
|
|
Another example is that the FLOAT datatypes are unsafe for hash
|
|
|
|
joins. On machines that meet the IEEE floating point standard, minus
|
|
|
|
zero and plus zero are different values (different bit patterns) but
|
|
|
|
they are defined to compare equal. So, if float equality were marked
|
|
|
|
HASHES, a minus zero and a plus zero would probably not be matched up
|
|
|
|
by a hash join, but they would be matched up by any other join process.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The bottom line is that you should probably only use HASHES for
|
|
|
|
equality operators that are (or could be) implemented by memcmp().
|
|
|
|
</para>
|
1999-04-08 15:29:08 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
</sect2>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<sect2>
|
|
|
|
<title>SORT1 and SORT2</title>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
The SORT clauses, if present, tell the system that it is permissible to use
|
|
|
|
the merge join method for a join based on the current operator.
|
|
|
|
Both must be specified if either is. The current operator must be
|
|
|
|
equality for some pair of data types, and the SORT1 and SORT2 clauses
|
|
|
|
name the ordering operator ('<' operator) for the left and right-side
|
|
|
|
data types respectively.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
Merge join is based on the idea of sorting the left and righthand tables
|
|
|
|
into order and then scanning them in parallel. So, both data types must
|
|
|
|
be capable of being fully ordered, and the join operator must be one
|
|
|
|
that can only succeed for pairs of values that fall at the "same place"
|
|
|
|
in the sort order. In practice this means that the join operator must
|
|
|
|
behave like equality. But unlike hashjoin, where the left and right
|
|
|
|
data types had better be the same (or at least bitwise equivalent),
|
|
|
|
it is possible to mergejoin two
|
|
|
|
distinct data types so long as they are logically compatible. For
|
|
|
|
example, the int2-versus-int4 equality operator is mergejoinable.
|
|
|
|
We only need sorting operators that will bring both datatypes into a
|
|
|
|
logically compatible sequence.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
When specifying merge sort operators, the current operator and both
|
|
|
|
referenced operators must return boolean; the SORT1 operator must have
|
|
|
|
both input datatypes equal to the current operator's left argument type,
|
|
|
|
and the SORT2 operator must have
|
|
|
|
both input datatypes equal to the current operator's right argument type.
|
|
|
|
(As with COMMUTATOR and NEGATOR, this means that the operator name is
|
|
|
|
sufficient to specify the operator, and the system is able to make dummy
|
|
|
|
operator entries if you happen to define the equality operator before
|
|
|
|
the other ones.)
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
In practice you should only write SORT clauses for an '=' operator,
|
|
|
|
and the two referenced operators should always be named '<'. Trying
|
|
|
|
to use merge join with operators named anything else will result in
|
|
|
|
hopeless confusion, for reasons we'll see in a moment.
|
|
|
|
</para>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-05-26 19:30:30 +02:00
|
|
|
<para>
|
|
|
|
There are additional restrictions on operators that you mark
|
|
|
|
mergejoinable. These restrictions are not currently checked by
|
|
|
|
CREATE OPERATOR, but a merge join may fail at runtime if any are
|
|
|
|
not true:
|
|
|
|
|
|
|
|
<itemizedlist>
|
|
|
|
<listitem>
|
|
|
|
<para>
|
|
|
|
The mergejoinable equality operator must have a commutator
|
|
|
|
(itself if the two data types are the same, or a related equality operator
|
|
|
|
if they are different).
|
|
|
|
</para>
|
|
|
|
</listitem>
|
|
|
|
|
|
|
|
<listitem>
|
|
|
|
<para>
|
|
|
|
There must be '<' and '>' ordering operators having the same left and
|
|
|
|
right input datatypes as the mergejoinable operator itself. These
|
|
|
|
operators <emphasis>must</emphasis> be named '<' and '>'; you do
|
|
|
|
not have any choice in the matter, since there is no provision for
|
|
|
|
specifying them explicitly. Note that if the left and right data types
|
|
|
|
are different, neither of these operators is the same as either
|
|
|
|
SORT operator. But they had better order the data values compatibly
|
|
|
|
with the SORT operators, or mergejoin will fail to work.
|
|
|
|
</para>
|
|
|
|
</listitem>
|
|
|
|
</itemizedlist>
|
|
|
|
</para>
|
|
|
|
</sect2>
|
1999-05-21 02:38:33 +02:00
|
|
|
|
1999-04-08 15:29:08 +02:00
|
|
|
</sect1>
|
|
|
|
</Chapter>
|
|
|
|
|
|
|
|
<!-- Keep this comment at the end of the file
|
|
|
|
Local variables:
|
|
|
|
mode: sgml
|
|
|
|
sgml-omittag:nil
|
|
|
|
sgml-shorttag:t
|
|
|
|
sgml-minimize-attributes:nil
|
|
|
|
sgml-always-quote-attributes:t
|
|
|
|
sgml-indent-step:1
|
|
|
|
sgml-indent-data:t
|
|
|
|
sgml-parent-document:nil
|
|
|
|
sgml-default-dtd-file:"./reference.ced"
|
|
|
|
sgml-exposed-tags:nil
|
|
|
|
sgml-local-catalogs:"/usr/lib/sgml/CATALOG"
|
|
|
|
sgml-local-ecat-files:nil
|
|
|
|
End:
|
|
|
|
-->
|