diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/__init__.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/pgjson.py | 128 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/psycopg2.py | 15 |
4 files changed, 148 insertions, 1 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index 3c1d19504..728f1629f 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -15,6 +15,7 @@ from .base import \ TSVECTOR from .constraints import ExcludeConstraint from .hstore import HSTORE, hstore +from .pgjson import JSON from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \ TSTZRANGE @@ -24,5 +25,5 @@ __all__ = ( 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN', 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE', 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE', - 'TSRANGE', 'TSTZRANGE', 'TSVECTOR' + 'TSRANGE', 'TSTZRANGE', 'json', 'JSON' ) diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index d3380afdd..a7f838009 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1246,6 +1246,9 @@ class PGTypeCompiler(compiler.GenericTypeCompiler): def visit_HSTORE(self, type_): return "HSTORE" + def visit_JSON(self, type_): + return "JSON" + def visit_INT4RANGE(self, type_): return "INT4RANGE" diff --git a/lib/sqlalchemy/dialects/postgresql/pgjson.py b/lib/sqlalchemy/dialects/postgresql/pgjson.py new file mode 100644 index 000000000..a29d0bbcc --- /dev/null +++ b/lib/sqlalchemy/dialects/postgresql/pgjson.py @@ -0,0 +1,128 @@ +# postgresql/json.py +# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import json + +from .base import ARRAY, ischema_names +from ... import types as sqltypes +from ...sql import functions as sqlfunc +from ...sql.operators import custom_op +from ... import util + +__all__ = ('JSON', 'json') + + +class JSON(sqltypes.TypeEngine): + """Represent the Postgresql JSON type. + + The :class:`.JSON` type stores arbitrary JSON format data, e.g.:: + + data_table = Table('data_table', metadata, + Column('id', Integer, primary_key=True), + Column('data', JSON) + ) + + with engine.connect() as conn: + conn.execute( + data_table.insert(), + data = {"key1": "value1", "key2": "value2"} + ) + + :class:`.JSON` provides several operations: + + * Index operations:: + + data_table.c.data['some key'] + + * Index operations returning text (required for text comparison or casting):: + + data_table.c.data.get_item_as_text('some key') == 'some value' + + * Path index operations:: + + data_table.c.data.get_path("{key_1, key_2, ..., key_n}") + + * Path index operations returning text (required for text comparison or casting):: + + data_table.c.data.get_path("{key_1, key_2, ..., key_n}") == 'some value' + + Please be aware that when used with the SQLAlchemy ORM, you will need to + replace the JSON object present on an attribute with a new object in order + for any changes to be properly persisted. + + .. versionadded:: 0.9 + """ + + __visit_name__ = 'JSON' + + def __init__(self, json_serializer=None, json_deserializer=None): + if json_serializer: + self.json_serializer = json_serializer + else: + self.json_serializer = json.dumps + if json_deserializer: + self.json_deserializer = json_deserializer + else: + self.json_deserializer = json.loads + + class comparator_factory(sqltypes.Concatenable.Comparator): + """Define comparison operations for :class:`.JSON`.""" + + def __getitem__(self, other): + """Text expression. Get the value at a given key.""" + # I'm choosing to return text here so the result can be cast, + # compared with strings, etc. + # + # The only downside to this is that you cannot dereference more + # than one level deep in json structures, though comparator + # support for multi-level dereference is lacking anyhow. + return self.expr.op('->', precedence=5)(other) + + def get_item_as_text(self, other): + """Text expression. Get the value at the given key as text. Use + this when you need to cast the type of the returned value.""" + return self.expr.op('->>', precedence=5)(other) + + def get_path(self, other): + """Text expression. Get the value at a given path. Paths are of + the form {key_1, key_2, ..., key_n}.""" + return self.expr.op('#>', precedence=5)(other) + + def get_path_as_text(self, other): + """Text expression. Get the value at a given path, as text. + Paths are of the form {key_1, key_2, ..., key_n}. Use this when + you need to cast the type of the returned value.""" + return self.expr.op('#>>', precedence=5)(other) + + def _adapt_expression(self, op, other_comparator): + if isinstance(op, custom_op): + if op.opstring == '->': + return op, sqltypes.Text + return sqltypes.Concatenable.Comparator.\ + _adapt_expression(self, op, other_comparator) + + def bind_processor(self, dialect): + if util.py2k: + encoding = dialect.encoding + def process(value): + return self.json_serializer(value).encode(encoding) + else: + def process(value): + return self.json_serializer(value) + return process + + def result_processor(self, dialect, coltype): + if util.py2k: + encoding = dialect.encoding + def process(value): + return self.json_deserializer(value.decode(encoding)) + else: + def process(value): + return self.json_deserializer(value) + return process + + +ischema_names['json'] = JSON diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index c3c749523..4a9248e5f 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -179,6 +179,7 @@ from .base import PGDialect, PGCompiler, \ ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES,\ _INT_TYPES from .hstore import HSTORE +from .pgjson import JSON logger = logging.getLogger('sqlalchemy.dialects.postgresql') @@ -233,6 +234,17 @@ class _PGHStore(HSTORE): else: return super(_PGHStore, self).result_processor(dialect, coltype) + +class _PGJSON(JSON): + # I've omitted the bind processor here because the method of serializing + # involves registering specific types to auto-serialize, and the adapter + # just a thin wrapper over json.dumps. + def result_processor(self, dialect, coltype): + if dialect._has_native_json: + return None + else: + return super(_PGJSON, self).result_processor(dialect, coltype) + # When we're handed literal SQL, ensure it's a SELECT-query. Since # 8.3, combining cursors and "FOR UPDATE" has been fine. SERVER_SIDE_CURSOR_RE = re.compile( @@ -317,6 +329,7 @@ class PGDialect_psycopg2(PGDialect): psycopg2_version = (0, 0) _has_native_hstore = False + _has_native_json = False colspecs = util.update_copy( PGDialect.colspecs, @@ -325,6 +338,7 @@ class PGDialect_psycopg2(PGDialect): ENUM: _PGEnum, # needs force_unicode sqltypes.Enum: _PGEnum, # needs force_unicode HSTORE: _PGHStore, + JSON: _PGJSON } ) @@ -352,6 +366,7 @@ class PGDialect_psycopg2(PGDialect): self._has_native_hstore = self.use_native_hstore and \ self._hstore_oids(connection.connection) \ is not None + self._has_native_json = self.psycopg2_version >= (2, 5) @classmethod def dbapi(cls): |