Skip to content

Commit

Permalink
- got firebird running
Browse files Browse the repository at this point in the history
- add some failure cases
- [bug] Firebird now uses strict "ansi bind rules"
so that bound parameters don't render in the
columns clause of a statement - they render
literally instead.

- [bug] Support for passing datetime as date when
using the DateTime type with Firebird; other
dialects support this.
  • Loading branch information
zzzeek committed Sep 23, 2012
1 parent e1d0985 commit 444abbe
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 43 deletions.
9 changes: 9 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,15 @@ underneath "0.7.xx".
no length is attempted to be emitted, same
way as MySQL. [ticket:2505]

- [bug] Firebird now uses strict "ansi bind rules"
so that bound parameters don't render in the
columns clause of a statement - they render
literally instead.

- [bug] Support for passing datetime as date when
using the DateTime type with Firebird; other
dialects support this.

- mysql
- [bug] Dialect no longer emits expensive server
collations query, as well as server casing,
Expand Down
24 changes: 20 additions & 4 deletions lib/sqlalchemy/dialects/firebird/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,36 @@
class _StringType(sqltypes.String):
"""Base for Firebird string types."""

def __init__(self, charset = None, **kw):
def __init__(self, charset=None, **kw):
self.charset = charset
super(_StringType, self).__init__(**kw)

class VARCHAR(_StringType, sqltypes.VARCHAR):
"""Firebird VARCHAR type"""
__visit_name__ = 'VARCHAR'

def __init__(self, length = None, **kwargs):
def __init__(self, length=None, **kwargs):
super(VARCHAR, self).__init__(length=length, **kwargs)

class CHAR(_StringType, sqltypes.CHAR):
"""Firebird CHAR type"""
__visit_name__ = 'CHAR'

def __init__(self, length = None, **kwargs):
def __init__(self, length=None, **kwargs):
super(CHAR, self).__init__(length=length, **kwargs)


class _FBDateTime(sqltypes.DateTime):
def bind_processor(self, dialect):
def process(value):
if type(value) == datetime.date:
return datetime.datetime(value.year, value.month, value.day)
else:
return value
return process

colspecs = {
sqltypes.DateTime: _FBDateTime
}

ischema_names = {
Expand Down Expand Up @@ -204,12 +215,17 @@ def visit_VARCHAR(self, type_):
class FBCompiler(sql.compiler.SQLCompiler):
"""Firebird specific idiosyncrasies"""

ansi_bind_rules = True

#def visit_contains_op_binary(self, binary, operator, **kw):
# cant use CONTAINING b.c. it's case insensitive.

#def visit_notcontains_op_binary(self, binary, operator, **kw):
# cant use NOT CONTAINING b.c. it's case insensitive.

def visit_now_func(self, fn, **kw):
return "CURRENT_TIMESTAMP"

def visit_startswith_op_binary(self, binary, operator, **kw):
return '%s STARTING WITH %s' % (
binary.left._compiler_dispatch(self, **kw),
Expand Down Expand Up @@ -261,7 +277,7 @@ def visit_length_func(self, function, **kw):

visit_char_length_func = visit_length_func

def function_argspec(self, func, **kw):
def _function_argspec(self, func, **kw):
# TODO: this probably will need to be
# narrowed to a fixed list, some no-arg functions
# may require parens - see similar example in the oracle
Expand Down
10 changes: 5 additions & 5 deletions test/engine/test_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def teardown_class(cls):
@testing.fails_on("postgresql+pg8000",
"pg8000 still doesn't allow single % without params")
def test_no_params_option(self):
stmt = "SELECT '%'"
if testing.against('oracle'):
stmt += " FROM DUAL"
stmt = "SELECT '%'" + testing.db.dialect.statement_compiler(
testing.db.dialect, None).default_from()

conn = testing.db.connect()
result = conn.\
execution_options(no_parameters=True).\
Expand Down Expand Up @@ -1181,7 +1181,7 @@ def cursor_execute(conn, cursor, statement, parameters,
('INSERT INTO t1 (c1, c2)', {
'c2': 'some data', 'c1': 5},
(5, 'some data')),
('SELECT lower', {'lower_2': 'Foo'},
('SELECT lower', {'lower_2': 'Foo'},
('Foo', )),
('INSERT INTO t1 (c1, c2)',
{'c2': 'foo', 'c1': 6},
Expand Down Expand Up @@ -1447,7 +1447,7 @@ def assert_stmts(expected, received):
('CREATE TABLE t1', {}, ()),
('INSERT INTO t1 (c1, c2)', {'c2': 'some data', 'c1'
: 5}, (5, 'some data')),
('SELECT lower', {'lower_2': 'Foo'},
('SELECT lower', {'lower_2': 'Foo'},
('Foo', )),
('INSERT INTO t1 (c1, c2)', {'c2': 'foo', 'c1': 6},
(6, 'foo')),
Expand Down
2 changes: 2 additions & 0 deletions test/lib/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ def selectone(fn):
return _chain_decorators_on(
fn,
skip_if(lambda: testing.against('oracle'),
"non-standard SELECT scalar syntax"),
skip_if(lambda: testing.against('firebird'),
"non-standard SELECT scalar syntax")
)

Expand Down
2 changes: 2 additions & 0 deletions test/orm/test_froms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,7 @@ def test_values_specific_order_by(self):
@testing.fails_on('postgresql+zxjdbc',
"zxjdbc parses the SQL itself before passing on "
"to PG, doesn't parse this")
@testing.fails_on("firebird", "unknown")
def test_values_with_boolean_selects(self):
"""Tests a values clause that works with select boolean
evaluations"""
Expand Down Expand Up @@ -1314,6 +1315,7 @@ def go():
eq_(results, [(User(name='jack'), 'jack')])
self.assert_sql_count(testing.db, go, 1)

@testing.fails_on("firebird", "unknown")
@testing.fails_on('postgresql+pg8000', "'type oid 705 not mapped to py type' (due to literal)")
def test_self_referential(self):
Order = self.classes.Order
Expand Down
12 changes: 1 addition & 11 deletions test/orm/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,16 +402,6 @@ def test_populate_existing(self):
assert u.addresses[0].email_address == '[email protected]'
assert u.orders[1].items[2].description == 'item 5'

@testing.fails_on_everything_except('sqlite', '+pyodbc', '+zxjdbc', 'mysql+oursql')
def test_query_str(self):
User = self.classes.User

s = create_session()
q = s.query(User).filter(User.id==1)
eq_(
str(q).replace('\n',''),
'SELECT users.id AS users_id, users.name AS users_name FROM users WHERE users.id = ?'
)

class InvalidGenerationsTest(QueryTest, AssertsCompiledSQL):
def test_no_limit_offset(self):
Expand Down Expand Up @@ -1499,7 +1489,7 @@ def test_union_mapped_colnames_preserved_across_subquery(self):
)


@testing.fails_on('mysql', "mysql doesn't support intersect")
@testing.requires.intersect
def test_intersect(self):
User = self.classes.User

Expand Down
20 changes: 14 additions & 6 deletions test/sql/test_case_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ def test_literal_interpretation(self):

assert_raises(exc.ArgumentError, case, [("x", "y")])

self.assert_compile(case([("x", "y")], value=t.c.col1), "CASE test.col1 WHEN :param_1 THEN :param_2 END")
self.assert_compile(case([(t.c.col1==7, "y")], else_="z"), "CASE WHEN (test.col1 = :col1_1) THEN :param_1 ELSE :param_2 END")
self.assert_compile(case([("x", "y")], value=t.c.col1),
"CASE test.col1 WHEN :param_1 THEN :param_2 END")
self.assert_compile(case([(t.c.col1 == 7, "y")], else_="z"),
"CASE WHEN (test.col1 = :col1_1) THEN :param_1 ELSE :param_2 END")

def test_text_doesnt_explode(self):

Expand All @@ -113,10 +115,16 @@ def test_text_doesnt_explode(self):
))]).order_by(info_table.c.info),

]:
eq_(s.execute().fetchall(), [
(u'no', ), (u'no', ), (u'no', ), (u'yes', ),
(u'no', ), (u'no', ),
])
if testing.against("firebird"):
eq_(s.execute().fetchall(), [
('no ', ), ('no ', ), ('no ', ), ('yes', ),
('no ', ), ('no ', ),
])
else:
eq_(s.execute().fetchall(), [
('no', ), ('no', ), ('no', ), ('yes', ),
('no', ), ('no', ),
])



Expand Down
8 changes: 3 additions & 5 deletions test/sql/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ def test_row_iteration(self):
l.append(row)
self.assert_(len(l) == 3)

@testing.fails_on('firebird', "kinterbasdb doesn't send full type information")
@testing.requires.subqueries
def test_anonymous_rows(self):
users.insert().execute(
Expand Down Expand Up @@ -710,7 +709,7 @@ def a_eq(executable, wanted):
use_labels=labels),
[(3, 'a'), (2, 'b'), (1, None)])

@testing.fails_on('mssql+pyodbc',
@testing.fails_on('mssql+pyodbc',
"pyodbc result row doesn't support slicing")
def test_column_slices(self):
users.insert().execute(user_id=1, user_name='john')
Expand Down Expand Up @@ -1203,7 +1202,6 @@ def test_bind_in(self):
assert len(r) == 0

@testing.emits_warning('.*empty sequence.*')
@testing.fails_on('firebird', 'uses sql-92 bind rules')
def test_literal_in(self):
"""similar to test_bind_in but use a bind with a value."""

Expand Down Expand Up @@ -1414,7 +1412,7 @@ def test_uppercase_direct_params_returning(self):
returning=(1, 5)
)

@testing.fails_on('mssql',
@testing.fails_on('mssql',
"lowercase table doesn't support identity insert disable")
def test_direct_params(self):
t = self._fixture()
Expand All @@ -1424,7 +1422,7 @@ def test_direct_params(self):
inserted_primary_key=[]
)

@testing.fails_on('mssql',
@testing.fails_on('mssql',
"lowercase table doesn't support identity insert disable")
@testing.requires.returning
def test_direct_params_returning(self):
Expand Down
34 changes: 22 additions & 12 deletions test/sql/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ def test_unicode_warnings(self):
# lambda: testing.db_spec("postgresql")(testing.db),
# "pg8000 and psycopg2 both have issues here in py3k"
# )
@testing.skip_if(lambda: testing.db_spec('mssql+mxodbc'),
@testing.skip_if(lambda: testing.db_spec('mssql+mxodbc'),
"unsupported behavior")
def test_ignoring_unicode_error(self):
"""checks String(unicode_error='ignore') is passed to underlying codec."""
Expand Down Expand Up @@ -1023,7 +1023,7 @@ class BinaryTest(fixtures.TestBase, AssertsExecutionResults):
__excluded_on__ = (
('mysql', '<', (4, 1, 1)), # screwy varbinary types
)

@classmethod
def setup_class(cls):
global binary_table, MyPickleType, metadata
Expand Down Expand Up @@ -1140,7 +1140,7 @@ def process(value):
return value / 10
return process
def adapt_operator(self, op):
return {operators.add:operators.sub,
return {operators.add:operators.sub,
operators.sub:operators.add}.get(op, op)

class MyTypeDec(types.TypeDecorator):
Expand Down Expand Up @@ -1498,18 +1498,18 @@ def setup_class(cls):
def teardown_class(cls):
users_with_date.drop()

def testdate(self):
def test_date_roundtrip(self):
global insert_data

l = map(tuple,
users_with_date.select().order_by(users_with_date.c.user_id).execute().fetchall())
self.assert_(l == insert_data,
'DateTest mismatch: got:%s expected:%s' % (l, insert_data))

def testtextdate(self):
def test_text_date_roundtrip(self):
x = testing.db.execute(text(
"select user_datetime from query_users_with_date",
typemap={'user_datetime':DateTime})).fetchall()
typemap={'user_datetime': DateTime})).fetchall()

self.assert_(isinstance(x[0][0], datetime.datetime))

Expand All @@ -1518,13 +1518,14 @@ def testtextdate(self):
bindparams=[bindparam('somedate', type_=types.DateTime)]),
somedate=datetime.datetime(2005, 11, 10, 11, 52, 35)).fetchall()

def testdate2(self):
def test_date_mixdatetime_roundtrip(self):
meta = MetaData(testing.db)
t = Table('testdate', meta,
Column('id', Integer,
Column('id', Integer,
Sequence('datetest_id_seq', optional=True),
primary_key=True),
Column('adate', Date), Column('adatetime', DateTime))
Column('adate', Date),
Column('adatetime', DateTime))
t.create(checkfirst=True)
try:
d1 = datetime.date(2007, 10, 30)
Expand All @@ -1540,8 +1541,14 @@ def testdate2(self):

# test mismatched date/datetime
t.insert().execute(adate=d2, adatetime=d2)
eq_(select([t.c.adate, t.c.adatetime], t.c.adate==d1).execute().fetchall(), [(d1, d2)])
eq_(select([t.c.adate, t.c.adatetime], t.c.adate==d1).execute().fetchall(), [(d1, d2)])
eq_(
select([t.c.adate, t.c.adatetime], t.c.adate == d1)\
.execute().fetchall(),
[(d1, d2)])
eq_(
select([t.c.adate, t.c.adatetime], t.c.adate == d1)\
.execute().fetchall(),
[(d1, d2)])

finally:
t.drop(checkfirst=True)
Expand Down Expand Up @@ -1705,6 +1712,9 @@ def test_many_significant_digits(self):
)
@testing.fails_on('postgresql+pg8000',
"pg-8000 does native decimal but truncates the decimals.")
@testing.fails_on("firebird",
"database and/or driver truncates decimal places."
)
def test_numeric_no_decimal(self):
numbers = set([
decimal.Decimal("1.000")
Expand Down Expand Up @@ -1762,7 +1772,7 @@ def test_float(self):
assert isinstance(val, float)

# some DBAPIs have unusual float handling
if testing.against('oracle+cx_oracle', 'mysql+oursql'):
if testing.against('oracle+cx_oracle', 'mysql+oursql', 'firebird'):
eq_(round_decimal(val, 3), 46.583)
else:
eq_(val, 46.583)
Expand Down

0 comments on commit 444abbe

Please sign in to comment.