# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
##
# Copyright (c) 2010-2014 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
Tests for the SQL Table definitions in txdav.common.datastore.sql_tables: sample
a couple of tables to make sure the schema is adequately parsed.

These aren't unit tests, they're integration tests to verify the behavior tested
by L{txdav.base.datastore.test.test_parseschema}.
"""

from cStringIO import StringIO

from twisted.python.modules import getModule
from twisted.trial.unittest import TestCase

from twext.enterprise.dal.syntax import SchemaSyntax

from txdav.common.datastore.sql_tables import schema, _translateSchema
from txdav.common.datastore.sql_tables import SchemaBroken, splitSQLString

from twext.enterprise.dal.test.test_parseschema import SchemaTestHelper

from textwrap import dedent

class SampleSomeColumns(TestCase, SchemaTestHelper):
    """
    Sample some columns from the tables defined by L{schema} and verify that
    they look correct.
    """

    def translated(self, *schema):
        """
        Translate the given schema (or the default schema if no schema given)
        and return the resulting SQL as a string.
        """
        io = StringIO()
        _translateSchema(io, *schema)
        return io.getvalue()


    def assertSortaEquals(self, a, b):
        """
        Assert that two strings are equals, modulo whitespace differences.
        """
        sortaA = " ".join(a.split())
        sortaB = " ".join(b.split())
        self.assertEquals(sortaA, sortaB)


    def test_addressbookObjectResourceID(self):
        ao = schema.ADDRESSBOOK_OBJECT
        self.assertEquals(ao.RESOURCE_ID.model.name,
                          "RESOURCE_ID")


    def test_schemaTranslation(self):
        """
        Basic integration test to make sure that the current, production schema
        can be translated without errors.
        """
        self.translated()


    def test_schemaTranslationIncludesVersion(self):
        """
        _translateSchema includes 'insert' rows too.
        """

        pathObj = (
            getModule(__name__).filePath
            .parent().sibling("sql_schema").child("current.sql")
        )
        schema = pathObj.getContent()
        pos = schema.find("('VERSION', '")
        version = int(schema[pos + 13:pos + 15])
        self.assertIn("insert into CALENDARSERVER (NAME, VALUE) "
                      "values ('VERSION', '%s');" % version,
                      self.translated())


    def test_translateSingleUnique(self):
        """
        L{_translateSchema} translates single-column 'unique' statements inline.
        """
        self.assertSortaEquals(
            self.translated(
                SchemaSyntax(
                    self.schemaFromString(
                        "create table alpha (beta integer unique)"
                    )
                )
            ),
            'create table alpha ( "beta" integer unique );'
        )


    def test_translateSingleTableUnique(self):
        """
        L{_translateSchema} translates single-column 'unique' statements inline,
        even if they were originally at the table level.
        """
        stx = SchemaSyntax(
            self.schemaFromString(
                "create table alpha (beta integer, unique(beta))"
            )
        )
        self.assertSortaEquals(
            self.translated(stx),
            'create table alpha ( "beta" integer unique );'
        )


    def test_multiTableUnique(self):
        """
        L{_translateSchema} translates multi-column 'unique' statements.
        """

        stx = SchemaSyntax(
            self.schemaFromString(
                "create table alpha ("
                "beta integer, gamma text, unique(beta, gamma))"
            )
        )
        self.assertSortaEquals(
            self.translated(stx),
            'create table alpha ( "beta" integer, "gamma" nclob, '
            'unique("beta", "gamma") );'
        )


    def test_translateSinglePrimaryKey(self):
        """
        L{_translateSchema} translates single-column 'primary key' statements
        inline.
        """
        self.assertSortaEquals(
            self.translated(
                SchemaSyntax(
                    self.schemaFromString(
                        "create table alpha (beta integer primary key)"
                    )
                )
            ),
            'create table alpha ( "beta" integer primary key );'
        )


    def test_translateSingleTablePrimaryKey(self):
        """
        L{_translateSchema} translates single-column 'primary key' statements
        inline, even if they were originally at the table level.
        """
        stx = SchemaSyntax(
            self.schemaFromString(
                "create table alpha (beta integer, primary key(beta))"
            )
        )
        self.assertSortaEquals(
            self.translated(stx),
            'create table alpha ( "beta" integer primary key );'
        )


    def test_multiTablePrimaryKey(self):
        """
        L{_translateSchema} translates multi-column 'primary key' statements.
        """

        stx = SchemaSyntax(
            self.schemaFromString(
                "create table alpha ("
                "beta integer, gamma text, primary key(beta, gamma))"
            )
        )
        self.assertSortaEquals(
            self.translated(stx),
            'create table alpha ( "beta" integer, "gamma" nclob, '
            'primary key("beta", "gamma") );'
        )


    def test_primaryKeyUniqueOrdering(self):
        """
        If a table specifies both a PRIMARY KEY and a UNIQUE constraint, the
        PRIMARY KEY will always be emitted first.
        """
        stx = SchemaSyntax(
            self.schemaFromString(
                "create table alpha ("
                "beta integer, gamma text, delta integer, "
                "unique(beta, delta), primary key(beta, gamma))"
            )
        )
        self.assertSortaEquals(
            self.translated(stx),
            'create table alpha ( '
            '"beta" integer, "gamma" nclob, "delta" integer, '
            'primary key("beta", "gamma"), unique("beta", "delta") );'
        )


    def test_anonymousCheckConstraint(self):
        """
        Named 'check' constraints are propagated through translation without
        modification.
        """
        self.assertSortaEquals(
            self.translated(SchemaSyntax(self.schemaFromString(
                            "create table alpha ( "
                            'beta integer, check(beta > 3)'
                            " );"
                        ))),
            "create table alpha ( "
            '"beta" integer, check("beta" > 3)'
            " );"
        )


    def test_namedCheckConstraint(self):
        """
        Named 'check' constraints are propagated through translation without
        modification.
        """
        self.assertSortaEquals(
            self.translated(SchemaSyntax(self.schemaFromString(
                            "create table alpha ( "
                            'beta integer, constraint beta_lt_3 check(beta > 3)'
                            " );"
                        ))),
            "create table alpha ( "
            '"beta" integer, constraint "beta_lt_3" check("beta" > 3)'
            " );"
        )


    def test_youBrokeTheSchema(self):
        """
        Oracle table names have a 30-character limit.  Our schema translator
        simply truncates the names if necessary, but that means that you can't
        have tables whose first 30 characters differ.  L{_translateSchema}
        raises a SchemaBroken() exception if this happens.  (This test is to
        make sure L{test_schemaTranslation}, above, will actually fail if this
        happens.)
        """
        # TODO: same thing for sequences.
        schema = self.schemaFromString(
            """
            create table same_012345678012345678990123456789_1 (foo integer);
            create table same_012345678012345678990123456789_2 (bar text);
            """
        )
        self.assertRaises(
            SchemaBroken, self.translated, SchemaSyntax(schema)
        )



class SQLSplitterTests(TestCase):
    """
    Test that strings which mix zero or more sql statements with zero or more
    pl/sql statements are split into individual statements.
    """

    def test_dontSplitOneStatement(self):
        """
        A single sql statement yields a single string
        """
        result = splitSQLString("select * from foo;")
        r1 = result.next()
        self.assertEquals(r1, "select * from foo")
        self.assertRaises(StopIteration, result.next)


    def test_returnTwoSimpleStatements(self):
        """
        Two simple sql statements yield two separate strings
        """
        result = splitSQLString("select count(*) from baz; select bang from boop;")
        r1 = result.next()
        self.assertEquals(r1, "select count(*) from baz")
        r2 = result.next()
        self.assertEquals(r2, "select bang from boop")
        self.assertRaises(StopIteration, result.next)


    def test_returnOneComplexStatement(self):
        """
        One complex sql statement yields a single string
        """
        bigSQL = dedent(
        '''SELECT
              CL.CODE,
              CL.CATEGORY,
           FROM
              CLIENTS_SUPPLIERS CL
              INVOICES I
           WHERE
              CL.CODE = I.CODE AND
              CL.CATEGORY = I.CATEGORY AND
              CL.UP_DATE =
                (SELECT
                   MAX(CL2.UP_DATE)
                 FROM
                   CLIENTS_SUPPLIERS CL2
                 WHERE
                   CL2.CODE = I.CODE AND
                   CL2.CATEGORY = I.CATEGORY AND
                   CL2.UP_DATE <= I.EMISSION
                ) AND
                I.EMISSION BETWEEN DATE1 AND DATE2;''')
        result = splitSQLString(bigSQL)
        r1 = result.next()
        self.assertEquals(r1, bigSQL.rstrip(";"))
        self.assertRaises(StopIteration, result.next)


    def test_returnOnePlSQL(self):
        """
        One pl/sql block yields a single string
        """
        plsql = dedent(
        '''BEGIN
           LOOP
               INSERT INTO T1 VALUES(i,i);
               i := i+1;
               EXIT WHEN i>100;
           END LOOP;
           END;''')
        s1 = 'BEGIN\nLOOP\nINSERT INTO T1 VALUES(i,i);i := i+1;EXIT WHEN i>100;END LOOP;END;'
        result = splitSQLString(plsql)
        r1 = result.next()
        self.assertEquals(r1, s1)
        self.assertRaises(StopIteration, result.next)


    def test_returnOnePlSQLAndOneSQL(self):
        """
        One sql statement and one pl/sql statement yields two separate strings
        """
        sql = dedent(
        '''SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
            FROM EGM
            INNER JOIN BioEntity
                ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId
            OPTION (MERGE JOIN);''')
        plsql = dedent(
        '''BEGIN
           FOR i IN 1..10 LOOP
               IF MOD(i,2) = 0 THEN
                   INSERT INTO temp VALUES (i, x, 'i is even');
               ELSE
                   INSERT INTO temp VALUES (i, x, 'i is odd');
               END IF;
               x := x + 100;
           END LOOP;
           COMMIT;
           END;''')
        s2 = "BEGIN\nFOR i IN 1..10 LOOP\nIF MOD(i,2) = 0 THEN\nINSERT INTO temp VALUES (i, x, 'i is even');ELSE\nINSERT INTO temp VALUES (i, x, 'i is odd');END IF;x := x + 100;END LOOP;COMMIT;END;"
        result = splitSQLString(sql + plsql)
        r1 = result.next()
        self.assertEquals(r1, sql.rstrip(";"))
        r2 = result.next()
        self.assertEquals(r2, s2)
        self.assertRaises(StopIteration, result.next)


    def test_actualSchemaUpgrade(self):
        """
        A real-world schema upgrade is split into the expected number of statements,
        ignoring comments
        """
        realsql = dedent(
        '''
        ----
        -- Copyright (c) 2011-2014 Apple Inc. All rights reserved.
        --
        -- Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
        --
        -- Unless required by applicable law or agreed to in writing, software
        -- distributed under the License is distributed on an "AS IS" BASIS,
        -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        -- See the License for the specific language governing permissions and
        -- limitations under the License.
        ----

        ---------------------------------------------------
        -- Upgrade database schema from VERSION 16 to 17 --
        ---------------------------------------------------


        ------------------------------
        -- CALENDAR_OBJECT clean-up --
        ------------------------------

        begin
        for i in (select constraint_name from user_cons_columns where column_name = 'ORGANIZER_OBJECT')
        loop
        execute immediate 'alter table calendar_object drop constraint ' || i.constraint_name;
        end loop;
        end;

        alter table CALENDAR_OBJECT
         drop (ORGANIZER_OBJECT);

        create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
            ICALENDAR_UID
        );


        -- Now update the version
        update CALENDARSERVER set VALUE = '17' where NAME = 'VERSION';
        ''')
        s1 = "begin\nfor i in (select constraint_name from user_cons_columns where column_name = 'ORGANIZER_OBJECT')\nloop\nexecute immediate 'alter table calendar_object drop constraint ' || i.constraint_name;end loop;end;"
        s2 = 'alter table CALENDAR_OBJECT\n drop (ORGANIZER_OBJECT)'
        s3 = 'create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (\n    ICALENDAR_UID\n)'
        s4 = "update CALENDARSERVER set VALUE = '17' where NAME = 'VERSION'"
        result = splitSQLString(realsql)
        r1 = result.next()
        self.assertEquals(r1, s1)
        r2 = result.next()
        self.assertEquals(r2, s2)
        r3 = result.next()
        self.assertEquals(r3, s3)
        r4 = result.next()
        self.assertEquals(r4, s4)
        self.assertRaises(StopIteration, result.next)
