389 lines
13 KiB
Python
389 lines
13 KiB
Python
##############################################################################
|
|
#
|
|
# Copyright (c) 2009 Zope Foundation and Contributors.
|
|
# All Rights Reserved.
|
|
#
|
|
# This software is subject to the provisions of the Zope Public License,
|
|
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
|
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
|
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE.
|
|
#
|
|
##############################################################################
|
|
"""IObjectMover implementation.
|
|
"""
|
|
|
|
|
|
from relstorage.adapters.interfaces import IObjectMover
|
|
|
|
from zope.interface import implementer
|
|
import os
|
|
import sys
|
|
|
|
from relstorage._compat import xrange
|
|
|
|
from ..mover import metricmethod_sampled
|
|
|
|
from .scriptrunner import format_to_named
|
|
from ..mover import AbstractObjectMover
|
|
|
|
def _to_oracle_ordered(query_tuple):
|
|
# Replace %s with :1, :2, etc
|
|
assert len(query_tuple) == 2
|
|
return format_to_named(query_tuple[0]), format_to_named(query_tuple[1])
|
|
|
|
|
|
@implementer(IObjectMover)
|
|
class OracleObjectMover(AbstractObjectMover):
|
|
|
|
# This is assigned to by the adapter.
|
|
inputsizes = None
|
|
|
|
_move_from_temp_hp_insert_query = format_to_named(AbstractObjectMover._move_from_temp_hp_insert_query)
|
|
_move_from_temp_hf_insert_query = format_to_named(AbstractObjectMover._move_from_temp_hf_insert_query)
|
|
_move_from_temp_copy_blob_query = format_to_named(AbstractObjectMover._move_from_temp_copy_blob_query)
|
|
|
|
_load_current_queries = _to_oracle_ordered(AbstractObjectMover._load_current_queries)
|
|
|
|
@metricmethod_sampled
|
|
def load_current(self, cursor, oid):
|
|
stmt = self._load_current_query
|
|
return self.runner.run_lob_stmt(
|
|
cursor, stmt, (oid,), default=(None, None))
|
|
|
|
|
|
_load_revision_query = format_to_named(AbstractObjectMover._load_revision_query)
|
|
|
|
@metricmethod_sampled
|
|
def load_revision(self, cursor, oid, tid):
|
|
stmt = self._load_revision_query
|
|
(state,) = self.runner.run_lob_stmt(
|
|
cursor, stmt, (oid, tid), default=(None,))
|
|
return state
|
|
|
|
|
|
_exists_queries = _to_oracle_ordered(AbstractObjectMover._exists_queries)
|
|
|
|
@metricmethod_sampled
|
|
def exists(self, cursor, oid):
|
|
stmt = self._exists_query
|
|
cursor.execute(stmt, (oid,))
|
|
for _row in cursor:
|
|
return True
|
|
return False
|
|
|
|
@metricmethod_sampled
|
|
def load_before(self, cursor, oid, tid):
|
|
"""Returns the pickle and tid of an object before transaction tid.
|
|
|
|
Returns (None, None) if no earlier state exists.
|
|
"""
|
|
stmt = """
|
|
SELECT state, tid
|
|
FROM object_state
|
|
WHERE zoid = :oid
|
|
AND tid = (
|
|
SELECT MAX(tid)
|
|
FROM object_state
|
|
WHERE zoid = :oid
|
|
AND tid < :tid
|
|
)
|
|
"""
|
|
return self.runner.run_lob_stmt(
|
|
cursor, stmt, {'oid': oid, 'tid': tid}, default=(None, None))
|
|
|
|
@metricmethod_sampled
|
|
def get_object_tid_after(self, cursor, oid, tid):
|
|
"""Returns the tid of the next change after an object revision.
|
|
|
|
Returns None if no later state exists.
|
|
"""
|
|
stmt = """
|
|
SELECT MIN(tid)
|
|
FROM object_state
|
|
WHERE zoid = :1
|
|
AND tid > :2
|
|
"""
|
|
cursor.execute(stmt, (oid, tid))
|
|
rows = cursor.fetchall()
|
|
if rows:
|
|
# XXX: If we can use rowcount here, we can combine
|
|
# with superclass.
|
|
assert len(rows) == 1
|
|
return rows[0][0]
|
|
else:
|
|
return None
|
|
|
|
# no store connection initialization needed for Oracle
|
|
def on_store_opened(self, cursor, restart=False):
|
|
pass
|
|
|
|
@metricmethod_sampled
|
|
def store_temp(self, cursor, batcher, oid, prev_tid, data):
|
|
"""Store an object in the temporary table."""
|
|
md5sum = self._compute_md5sum(data)
|
|
|
|
size = len(data)
|
|
if size <= 2000:
|
|
# Send data inline for speed. Oracle docs say maximum size
|
|
# of a RAW is 2000 bytes.
|
|
stmt = "BEGIN relstorage_op.store_temp(:1, :2, :3, :4); END;"
|
|
batcher.add_array_op(
|
|
stmt,
|
|
'oid prev_tid md5sum rawdata',
|
|
(oid, prev_tid, md5sum, data),
|
|
rowkey=oid,
|
|
size=size,
|
|
)
|
|
else:
|
|
# Send data as a BLOB
|
|
row = {
|
|
'oid': oid,
|
|
'prev_tid': prev_tid,
|
|
'md5sum': md5sum,
|
|
'blobdata': data,
|
|
}
|
|
batcher.insert_into(
|
|
"temp_store (zoid, prev_tid, md5, state)",
|
|
":oid, :prev_tid, :md5sum, :blobdata",
|
|
row,
|
|
rowkey=oid,
|
|
size=size,
|
|
)
|
|
|
|
@metricmethod_sampled
|
|
def restore(self, cursor, batcher, oid, tid, data):
|
|
"""Store an object directly, without conflict detection.
|
|
|
|
Used for copying transactions into this database.
|
|
"""
|
|
md5sum = self._compute_md5sum(data)
|
|
|
|
size = len(data) if data is not None else 0
|
|
|
|
if size <= 2000:
|
|
# Send data inline for speed. Oracle docs say maximum size
|
|
# of a RAW is 2000 bytes.
|
|
if self.keep_history:
|
|
stmt = "BEGIN relstorage_op.restore(:1, :2, :3, :4); END;"
|
|
batcher.add_array_op(
|
|
stmt,
|
|
'oid tid md5sum rawdata',
|
|
(oid, tid, md5sum, data),
|
|
rowkey=(oid, tid),
|
|
size=size,
|
|
)
|
|
else:
|
|
stmt = "BEGIN relstorage_op.restore(:1, :2, :3); END;"
|
|
batcher.add_array_op(
|
|
stmt,
|
|
'oid tid rawdata',
|
|
(oid, tid, data),
|
|
rowkey=(oid, tid),
|
|
size=size,
|
|
)
|
|
|
|
else:
|
|
# Send as a BLOB
|
|
if self.keep_history:
|
|
row = {
|
|
'oid': oid,
|
|
'tid': tid,
|
|
'md5sum': md5sum,
|
|
'state_size': size,
|
|
'blobdata': data,
|
|
}
|
|
row_schema = """
|
|
:oid, :tid,
|
|
COALESCE((SELECT tid
|
|
FROM current_object
|
|
WHERE zoid = :oid), 0),
|
|
:md5sum, :state_size, :blobdata
|
|
"""
|
|
batcher.insert_into(
|
|
"object_state (zoid, tid, prev_tid, md5, state_size, state)",
|
|
row_schema,
|
|
row,
|
|
rowkey=(oid, tid),
|
|
size=size,
|
|
)
|
|
else:
|
|
batcher.delete_from('object_state', zoid=oid)
|
|
if data:
|
|
row = {
|
|
'oid': oid,
|
|
'tid': tid,
|
|
'state_size': size,
|
|
'blobdata': data,
|
|
}
|
|
batcher.insert_into(
|
|
"object_state (zoid, tid, state_size, state)",
|
|
":oid, :tid, :state_size, :blobdata",
|
|
row,
|
|
rowkey=oid,
|
|
size=size,
|
|
)
|
|
|
|
@metricmethod_sampled
|
|
def replace_temp(self, cursor, oid, prev_tid, data):
|
|
"""Replace an object in the temporary table.
|
|
|
|
This happens after conflict resolution.
|
|
"""
|
|
md5sum = self._compute_md5sum(data)
|
|
|
|
stmt = """
|
|
UPDATE temp_store SET
|
|
prev_tid = :prev_tid,
|
|
md5 = :md5sum,
|
|
state = :blobdata
|
|
WHERE zoid = :oid
|
|
"""
|
|
cursor.setinputsizes(blobdata=self.inputsizes['blobdata']) # pylint:disable=unsubscriptable-object
|
|
cursor.execute(stmt, oid=oid, prev_tid=prev_tid,
|
|
md5sum=md5sum, blobdata=self.Binary(data))
|
|
|
|
|
|
|
|
_update_current_insert_query = format_to_named(AbstractObjectMover._update_current_insert_query)
|
|
_update_current_update_query = format_to_named(AbstractObjectMover._update_current_update_query)
|
|
_update_current_update_query = _update_current_update_query.replace('ORDER BY zoid', '')
|
|
|
|
@metricmethod_sampled
|
|
def download_blob(self, cursor, oid, tid, filename):
|
|
"""Download a blob into a file."""
|
|
stmt = """
|
|
SELECT chunk
|
|
FROM blob_chunk
|
|
WHERE zoid = :1
|
|
AND tid = :2
|
|
ORDER BY chunk_num
|
|
"""
|
|
|
|
f = None
|
|
bytecount = 0
|
|
# Current versions of cx_Oracle only support offsets up
|
|
# to sys.maxint or 4GB, whichever comes first.
|
|
maxsize = min(sys.maxsize, 1 << 32)
|
|
try:
|
|
cursor.execute(stmt, (oid, tid))
|
|
while True:
|
|
try:
|
|
blob, = cursor.fetchone()
|
|
except TypeError:
|
|
# No more chunks. Note: if there are no chunks at
|
|
# all, then this method should not write a file.
|
|
break
|
|
|
|
if f is None:
|
|
f = open(filename, 'wb')
|
|
# round off the chunk-size to be a multiple of the oracle
|
|
# blob chunk size to maximize performance
|
|
read_chunk_size = int(
|
|
max(
|
|
round(1.0 * self.blob_chunk_size / blob.getchunksize()),
|
|
1)
|
|
* blob.getchunksize())
|
|
offset = 1 # Oracle still uses 1-based indexing.
|
|
reader = iter(lambda: blob.read(offset, read_chunk_size), b'')
|
|
for read_chunk in reader:
|
|
f.write(read_chunk)
|
|
bytecount += len(read_chunk)
|
|
offset += len(read_chunk)
|
|
if offset > maxsize:
|
|
# We have already read the maximum we can store
|
|
# so we can assume we are done. If we do not break
|
|
# off here, cx_Oracle will throw an overflow
|
|
# exception anyway.
|
|
break
|
|
except:
|
|
if f is not None:
|
|
f.close()
|
|
os.remove(filename)
|
|
raise
|
|
|
|
if f is not None:
|
|
f.close()
|
|
return bytecount
|
|
|
|
# Current versions of cx_Oracle only support offsets up
|
|
# to sys.maxint or 4GB, whichever comes first. We divide up our
|
|
# upload into chunks within this limit.
|
|
oracle_blob_chunk_maxsize = min(sys.maxsize, 1 << 32)
|
|
|
|
@metricmethod_sampled
|
|
def upload_blob(self, cursor, oid, tid, filename):
|
|
"""Upload a blob from a file.
|
|
|
|
If serial is None, upload to the temporary table.
|
|
"""
|
|
# pylint:disable=too-many-locals
|
|
if tid is not None:
|
|
if self.keep_history:
|
|
delete_stmt = """
|
|
DELETE FROM blob_chunk
|
|
WHERE zoid = :1 AND tid = :2
|
|
"""
|
|
cursor.execute(delete_stmt, (oid, tid))
|
|
else:
|
|
delete_stmt = "DELETE FROM blob_chunk WHERE zoid = :1"
|
|
cursor.execute(delete_stmt, (oid,))
|
|
|
|
use_tid = True
|
|
insert_stmt = """
|
|
INSERT INTO blob_chunk (zoid, tid, chunk_num, chunk)
|
|
VALUES (:oid, :tid, :chunk_num, empty_blob())
|
|
"""
|
|
select_stmt = """
|
|
SELECT chunk FROM blob_chunk
|
|
WHERE zoid=:oid AND tid=:tid AND chunk_num=:chunk_num
|
|
"""
|
|
|
|
else:
|
|
use_tid = False
|
|
delete_stmt = "DELETE FROM temp_blob_chunk WHERE zoid = :1"
|
|
cursor.execute(delete_stmt, (oid,))
|
|
|
|
insert_stmt = """
|
|
INSERT INTO temp_blob_chunk (zoid, chunk_num, chunk)
|
|
VALUES (:oid, :chunk_num, empty_blob())
|
|
"""
|
|
select_stmt = """
|
|
SELECT chunk FROM temp_blob_chunk
|
|
WHERE zoid=:oid AND chunk_num=:chunk_num
|
|
"""
|
|
|
|
f = open(filename, 'rb')
|
|
maxsize = self.oracle_blob_chunk_maxsize
|
|
try:
|
|
chunk_num = 0
|
|
while True:
|
|
blob = None
|
|
params = dict(oid=oid, chunk_num=chunk_num)
|
|
if use_tid:
|
|
params['tid'] = tid
|
|
cursor.execute(insert_stmt, params)
|
|
cursor.execute(select_stmt, params)
|
|
blob, = cursor.fetchone()
|
|
blob.open()
|
|
write_chunk_size = int(
|
|
max(
|
|
round(1.0 * self.blob_chunk_size / blob.getchunksize()),
|
|
1)
|
|
* blob.getchunksize())
|
|
offset = 1 # Oracle still uses 1-based indexing.
|
|
for _i in xrange(maxsize // write_chunk_size):
|
|
write_chunk = f.read(write_chunk_size)
|
|
if not blob.write(write_chunk, offset):
|
|
# EOF.
|
|
return
|
|
offset += len(write_chunk)
|
|
if blob is not None and blob.isopen():
|
|
blob.close()
|
|
chunk_num += 1
|
|
finally:
|
|
f.close()
|
|
if blob is not None and blob.isopen():
|
|
blob.close()
|