Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added an Oracle adapter that works with the Oracle bindings by Yoshida (

http://raa.ruby-lang.org/project/oracle/) #564 [Maik Schmidt]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@522 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 336c2cbb8f728825139f2ab94b96726935ea18a1 1 parent 8382e6e
@dhh dhh authored
View
15 activerecord/CHANGELOG
@@ -1,5 +1,20 @@
*SVN*
+* Added an Oracle adapter that works with the Oracle bindings by Yoshida (http://raa.ruby-lang.org/project/oracle/) #564 [Maik Schmidt]
+
+ A number of caveats:
+
+ 1. You have to create a sequence for every table that need an auto-generated primary key.
+ The sequence name is built by appending "_id" to the according table_name,
+ i.e. if you have a table called "employees", you have to create a sequence called "employees_id".
+
+ 2. The data type TIME is not supported, because Oracle does not support it.
+
+ 3. Binary data (BLOBs etc.) are currently not supported. Will be fixed soon.
+
+ 4. LIMIT clauses with OFFSET are not supported, because I could not find a way to simulate this behaviour using ROWNUM and "SELECT *".
+
+
* Fixed Base#clone for use with PostgreSQL #565 [hanson@surgery.wisc.edu]
View
6 activerecord/Rakefile
@@ -63,6 +63,12 @@ Rake::TestTask.new("test_db2") { |t|
t.verbose = true
}
+Rake::TestTask.new("test_oracle") { |t|
+ t.libs << "test" << "test/connections/native_oracle"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
# Generate the RDoc documentation
Rake::RDocTask.new { |rdoc|
View
2  activerecord/install.rb
@@ -37,6 +37,7 @@
active_record/connection_adapters/abstract_adapter.rb
active_record/connection_adapters/db2_adapter.rb
active_record/connection_adapters/mysql_adapter.rb
+ active_record/connection_adapters/oracle_adapter.rb
active_record/connection_adapters/postgresql_adapter.rb
active_record/connection_adapters/sqlite_adapter.rb
active_record/connection_adapters/sqlserver_adapter.rb
@@ -65,6 +66,7 @@
active_record/timestamp.rb
active_record/transactions.rb
active_record/validations.rb
+ active_record/vendor/db2.rb
active_record/vendor/mysql.rb
active_record/vendor/mysql411.rb
active_record/vendor/simple.rb
View
1  activerecord/lib/active_record.rb
@@ -60,3 +60,4 @@
require 'active_record/connection_adapters/sqlite_adapter'
require 'active_record/connection_adapters/sqlserver_adapter'
require 'active_record/connection_adapters/db2_adapter'
+require 'active_record/connection_adapters/oracle_adapter'
View
42 activerecord/lib/active_record/connection_adapters/db2_adapter.rb
@@ -1,5 +1,4 @@
-# db2_adapter.rb
-# author: Maik Schmidt <contact@maik-schmidt.de>
+# Author: Maik Schmidt <contact@maik-schmidt.de>
require 'active_record/connection_adapters/abstract_adapter'
@@ -100,30 +99,31 @@ def columns(table_name, name = nil)
end
private
- def last_insert_id
- row = select_one(<<-GETID.strip)
- with temp(id) as (values (identity_val_local())) select * from temp
- GETID
- row['id'].to_i
+
+ def last_insert_id
+ row = select_one(<<-GETID.strip)
+ with temp(id) as (values (identity_val_local())) select * from temp
+ GETID
+ row['id'].to_i
+ end
+
+ def select(sql, name = nil)
+ stmt = nil
+ log(sql, name, @connection) do |connection|
+ stmt = DB2::Statement.new(connection)
+ stmt.exec_direct(sql + " with ur")
end
- def select(sql, name = nil)
- stmt = nil
- log(sql, name, @connection) do |connection|
- stmt = DB2::Statement.new(connection)
- stmt.exec_direct(sql + " with ur")
- end
-
- rows = []
- while row = stmt.fetch_as_hash
- rows << row
- end
- stmt.free
- rows
+ rows = []
+ while row = stmt.fetch_as_hash
+ rows << row
end
+ stmt.free
+ rows
+ end
end
end
end
rescue LoadError
# DB2 driver is unavailable.
-end
+end
View
244 activerecord/lib/active_record/connection_adapters/oracle_adapter.rb
@@ -0,0 +1,244 @@
+# Author: Maik Schmidt <contact@maik-schmidt.de>
+require 'active_record/connection_adapters/abstract_adapter'
+require 'date'
+
+begin
+ require 'oracle' unless self.class.const_defined?(:ORAconn)
+
+ module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by
+ # all Active Record objects
+ def self.oracle_connection(config) # :nodoc:
+ symbolize_strings_in_hash(config)
+ usr = config[:username] || ''
+ pwd = config[:password] || ''
+ db = config[:database] || ''
+
+ connection = ORAconn.logon(usr, pwd, db)
+ cursor = connection.open
+ cursor.parse("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'")
+ rows_affected = cursor.exec
+ cursor.close
+ ConnectionAdapters::OracleAdapter.new(connection)
+ end
+ end
+
+ module ConnectionAdapters
+ class OracleAdapter < AbstractAdapter
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ select(sql, name).first
+ end
+
+ # Oracle does not support auto-generated columns, so we have to use
+ # sequences. Every insert is followed by a select statement, which
+ # returns the current sequence value. The statements are encapsulated
+ # in an anonymous PL/SQL block (supported since Oracle 7.3) to prevent
+ # race conditions and to maximize performance.
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ new_id = nil
+ if !id_value.nil?
+ execute(sql, name)
+ new_id = id_value
+ else
+ pk_col = pk || ActiveRecord::Base::primary_key
+ if sql !~ Regexp.new('\b' + pk_col + '\b')
+ seq_name = sql.sub(/^\s*insert\s+?into\s+(\S+).*$/im, '\1') + "_id"
+ sql.sub!(/(into\s+.*?\()/im, '\1' + "#{pk_col}, ")
+ sql.sub!(/(values\s*\()/im, '\1' + "#{seq_name}.nextval, ")
+ new_id = ' ' * 40 # Enough space for String representation of ID?
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ s = "begin #{sql}; select #{seq_name}.currval into :new_id from dual; end;"
+ cursor.parse(s)
+ cursor.bindrv(':new_id', new_id)
+ cursor.exec
+ cursor.close
+ new_id = new_id.to_i
+ end
+ else
+ execute(sql, name)
+ end
+ end
+ new_id
+ end
+
+ def execute(sql, name = nil)
+ rows_affected = 0
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ cursor.parse(sql)
+ rows_affected = cursor.exec
+ cursor.close
+ end
+ rows_affected
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ @connection.commitoff
+ end
+
+ def commit_db_transaction
+ @connection.commit
+ @connection.commiton
+ end
+
+ def rollback_db_transaction
+ @connection.rollback
+ @connection.commiton
+ end
+
+ def quote_column_name(name) name; end
+
+ def quote(value, column = nil)
+ if column && column.type == :timestamp && value.class.to_s == "String"
+ begin
+ value = DateTime.parse(value)
+ rescue => ex
+ # Value cannot be parsed.
+ end
+ end
+
+ case value
+ when String
+ if column && column.type == :binary
+ "'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
+ else
+ "'#{quote_string(value)}'" # ' (for ruby-mode)
+ end
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? "'t'" : "1")
+ when FalseClass then (column && column.type == :boolean ? "'f'" : "0")
+ when Float, Fixnum, Bignum then value.to_s
+ when Date then "to_date('#{value.strftime("%Y-%m-%d")}', 'YYYY-MM-DD')"
+ when Time, DateTime then "to_date('#{value.strftime("%Y-%m-%d %H:%M:%S")}', 'YYYY-MM-DD HH24:MI:SS')"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def add_limit!(sql, limit)
+ l, o = limit.to_s.scan(/\d+/)
+ if o.nil?
+ sql.sub!(/^.*$/im, "select * from (#{sql}) where rownum <= #{l}")
+ else
+ raise ArgumentError, "LIMIT clauses with OFFSET are not supported yet!"
+ end
+ end
+
+ def columns(table_name, name = nil)
+ sql = <<-SQL
+ select column_name,
+ data_type,
+ data_length,
+ data_precision,
+ data_default
+ from user_tab_columns
+ where table_name = upper('#{table_name}')
+ SQL
+ result = []
+ cols = select_all(sql, name)
+ cols.each do |c|
+ name = c['column_name'].downcase
+ default = c['data_default']
+ default = (default == 'NULL') ? nil : default
+ type = get_sql_type(c['data_type'], c['data_length'])
+ result << Column.new(name, default, type)
+ end
+ result
+ end
+
+ private
+
+ def get_sql_type(type_name, type_length)
+ case type_name
+ when /timestamp/i
+ return "TIMESTAMP"
+ when /number/i
+ return "INT"
+ when /date/i
+ return "DATE"
+ else
+ return "#{type_name}(#{type_length})"
+ end
+ end
+
+ def select(sql, name = nil)
+ col_names = []
+ cursor = nil
+ log(sql, name, @connection) do |connection|
+ cursor = connection.open
+ col_names = parse(cursor, sql)
+ cursor.exec
+ end
+
+ rows = []
+ while cursor.fetch do
+ row = {}
+ col_names.each_with_index do |name, i|
+ row[name] = cursor.getCol(i + 1)[0]
+ end
+ rows << row
+ end
+ cursor.close
+ rows
+ end
+
+ VARCHAR2 = 1
+ NUMBER = 2
+ INTEGER = 3 ## external
+ FLOAT = 4 ## external
+ LONG = 8
+ ROWID = 11
+ DATE = 12
+ RAW = 23
+ LONG_RAW = 24
+ UNSIGNED_INT = 68 ## external
+ CHAR = 96
+ MLSLABEL = 105
+
+ def parse(cursor, sql)
+ cursor.parse(sql)
+ colnr = 1
+ col_names = []
+ loop {
+ colinfo = cursor.describe(colnr)
+ break if colinfo.nil?
+
+ col_names << colinfo[2].downcase
+ collength, coltype = colinfo[3], colinfo[1]
+
+ collength, coltype = case coltype
+ when NUMBER
+ [40, VARCHAR2]
+ when VARCHAR2, CHAR
+ [(collength * 1.5).ceil, VARCHAR2]
+ when LONG
+ [65535, LONG]
+ when LONG_RAW
+ [65535, LONG_RAW]
+ else
+ [collength, VARCHAR2]
+ end
+
+ cursor.define(colnr, collength, coltype)
+ colnr += 1
+ }
+ col_names
+ end
+ end
+ end
+ end
+rescue LoadError
+ # Oracle driver is unavailable.
+end
View
14 activerecord/test/base_test.rb
@@ -176,6 +176,10 @@ def test_preserving_date_objects
"The last_read attribute should be of the Date class"
)
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
assert_kind_of(
Time, Topic.find(1).bonus_time,
"The bonus_time attribute should be of the Time class"
@@ -381,6 +385,11 @@ def test_default_values
end
def test_utc_as_time_zone
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
+
Topic.default_timezone = :utc
attributes = { "bonus_time" => "5:42:00AM" }
topic = Topic.find(1)
@@ -506,6 +515,11 @@ def test_multiparameter_attributes_on_time_with_empty_seconds
end
def test_attributes_on_dummy_time
+ # Oracle does not have a TIME datatype.
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
+
attributes = {
"bonus_time" => "5:42:00AM"
}
View
8 activerecord/test/binary_test.rb
@@ -13,6 +13,10 @@ def test_load_save
if ActiveRecord::ConnectionAdapters.const_defined? :DB2Adapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::DB2Adapter)
end
+
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ end
bin = Binary.new
bin.data = @data
@@ -27,10 +31,10 @@ def test_load_save
db_bin = Binary.find(bin.id)
assert db_bin.data == bin.data,
- "Loaded binary data differes from memory version"
+ "Loaded binary data differs from memory version"
assert db_bin.data == File.new(File.dirname(__FILE__)+"/fixtures/associations.png","rb").read,
- "Loaded binary data differes from file version"
+ "Loaded binary data differs from file version"
end
private
View
11 activerecord/test/column_alias_test.rb
@@ -5,8 +5,15 @@ class TestColumnAlias < Test::Unit::TestCase
def test_column_alias
topic = Topic.find(1)
- records = topic.connection.select_all("SELECT id AS pk FROM topics LIMIT 1")
- assert_equal(records[0].keys[0], "pk")
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ records = topic.connection.select_all("SELECT id AS pk FROM topics WHERE ROWNUM < 2")
+ assert_equal(records[0].keys[0], "pk")
+ end
+ else
+ records = topic.connection.select_all("SELECT id AS pk FROM topics LIMIT 1")
+ assert_equal(records[0].keys[0], "pk")
+ end
end
end
View
24 activerecord/test/connections/native_oracle/connection.rb
@@ -0,0 +1,24 @@
+print "Using native Oracle\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'local'
+db2 = 'local'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "oracle",
+ :host => "localhost",
+ :username => "arunit",
+ :password => "arunit",
+ :database => db1
+)
+
+Course.establish_connection(
+ :adapter => "oracle",
+ :host => "localhost",
+ :username => "arunit2",
+ :password => "arunit2",
+ :database => db2
+)
View
14 activerecord/test/finder_test.rb
@@ -34,10 +34,16 @@ def test_find_all_with_limit
end
def test_find_all_with_prepared_limit_and_offset
- entrants = Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1]
-
- assert_equal(2, entrants.size)
- assert_equal(@entrants["second"]["name"], entrants.first.name)
+ if ActiveRecord::ConnectionAdapters.const_defined? :OracleAdapter
+ if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::OracleAdapter)
+ assert_raises(ArgumentError) { Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1] }
+ end
+ else
+ entrants = Entrant.find_all nil, "id ASC", ["? OFFSET ?", 2, 1]
+
+ assert_equal(2, entrants.size)
+ assert_equal(@entrants["second"]["name"], entrants.first.name)
+ end
end
def test_find_with_entire_select_statement
View
5 activerecord/test/fixtures/db_definitions/create_oracle_db.bat
@@ -0,0 +1,5 @@
+sqlplus arunit/arunit @ drop_oracle_tables
+sqlplus arunit/arunit @ oracle
+sqlplus arunit2/arunit2 @ drop_oracle_tables2
+sqlplus arunit2/arunit2 @ oracle2
+
View
5 activerecord/test/fixtures/db_definitions/create_oracle_db.sh
@@ -0,0 +1,5 @@
+sqlplus arunit/arunit @ drop_oracle_tables
+sqlplus arunit/arunit @ oracle
+sqlplus arunit2/arunit2 @ drop_oracle_tables2
+sqlplus arunit2/arunit2 @ oracle2
+
View
35 activerecord/test/fixtures/db_definitions/drop_oracle_tables.sql
@@ -0,0 +1,35 @@
+DROP TABLE accounts;
+DROP SEQUENCE accounts_id;
+DROP TABLE companies;
+DROP SEQUENCE companies_id;
+DROP TABLE topics;
+DROP SEQUENCE topics_id;
+DROP TABLE developers;
+DROP SEQUENCE developers_id;
+DROP TABLE projects;
+DROP SEQUENCE projects_id;
+DROP TABLE developers_projects;
+DROP SEQUENCE developers_projects_id;
+DROP TABLE customers;
+DROP SEQUENCE customers_id;
+DROP TABLE movies;
+DROP SEQUENCE movies_id;
+DROP TABLE subscribers;
+DROP SEQUENCE subscribers_id;
+DROP TABLE booleantests;
+DROP SEQUENCE booleantests_id;
+DROP TABLE auto_id_tests;
+DROP SEQUENCE auto_id_tests_id;
+DROP TABLE entrants;
+DROP SEQUENCE entrants_id;
+DROP TABLE colnametests;
+DROP SEQUENCE colnametests_id;
+DROP TABLE mixins;
+DROP SEQUENCE mixins_id;
+DROP TABLE people;
+DROP SEQUENCE people_id;
+DROP TABLE binaries;
+DROP SEQUENCE binaries_id;
+DROP TABLE computers;
+DROP SEQUENCE computers_id;
+EXIT;
View
3  activerecord/test/fixtures/db_definitions/drop_oracle_tables2.sql
@@ -0,0 +1,3 @@
+DROP TABLE courses;
+DROP SEQUENCE courses_id;
+EXIT;
View
149 activerecord/test/fixtures/db_definitions/oracle.sql
@@ -0,0 +1,149 @@
+CREATE TABLE accounts (
+ id int NOT NULL,
+ firm_id int default NULL,
+ credit_limit int default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE accounts_id MINVALUE 10000;
+
+CREATE TABLE companies (
+ id int NOT NULL,
+ type varchar(50) default NULL,
+ ruby_type varchar(50) default NULL,
+ firm_id int default NULL,
+ name varchar(50) default NULL,
+ client_of int default NULL,
+ rating int default 1,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE companies_id MINVALUE 10000;
+
+CREATE TABLE topics (
+ id int NOT NULL,
+ title varchar(255) default NULL,
+ author_name varchar(255) default NULL,
+ author_email_address varchar(255) default NULL,
+ written_on timestamp default NULL,
+ bonus_time timestamp default NULL,
+ last_read date default NULL,
+ content varchar(3000),
+ approved smallint default 1,
+ replies_count int default 0,
+ parent_id int default NULL,
+ type varchar(50) default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE topics_id MINVALUE 10000;
+
+CREATE TABLE developers (
+ id int NOT NULL,
+ name varchar(100) default NULL,
+ salary int default 70000,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE developers_id MINVALUE 10000;
+
+CREATE TABLE projects (
+ id int NOT NULL,
+ name varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE projects_id MINVALUE 10000;
+
+CREATE TABLE developers_projects (
+ developer_id int NOT NULL,
+ project_id int NOT NULL,
+ joined_on date default NULL
+);
+CREATE SEQUENCE developers_projects_id MINVALUE 10000;
+
+CREATE TABLE customers (
+ id int NOT NULL,
+ name varchar(100) default NULL,
+ balance int default 0,
+ address_street varchar(100) default NULL,
+ address_city varchar(100) default NULL,
+ address_country varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE customers_id MINVALUE 10000;
+
+CREATE TABLE movies (
+ movieid int NOT NULL,
+ name varchar(100) default NULL,
+ PRIMARY KEY (movieid)
+);
+CREATE SEQUENCE movies_id MINVALUE 10000;
+
+CREATE TABLE subscribers (
+ nick varchar(100) NOT NULL,
+ name varchar(100) default NULL,
+ PRIMARY KEY (nick)
+);
+CREATE SEQUENCE subscribers_id MINVALUE 10000;
+
+CREATE TABLE booleantests (
+ id int NOT NULL,
+ value int default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE booleantests_id MINVALUE 10000;
+
+CREATE TABLE auto_id_tests (
+ auto_id int NOT NULL,
+ value int default NULL,
+ PRIMARY KEY (auto_id)
+);
+CREATE SEQUENCE auto_id_tests_id MINVALUE 10000;
+
+CREATE TABLE entrants (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL,
+ course_id int NOT NULL
+);
+CREATE SEQUENCE entrants_id MINVALUE 10000;
+
+CREATE TABLE colnametests (
+ id int NOT NULL,
+ references int NOT NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE colnametests_id MINVALUE 10000;
+
+CREATE TABLE mixins (
+ id int NOT NULL,
+ parent_id int default NULL,
+ pos int default NULL,
+ created_at timestamp default NULL,
+ updated_at timestamp default NULL,
+ lft int default NULL,
+ rgt int default NULL,
+ root_id int default NULL,
+ type varchar(40) default NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE mixins_id MINVALUE 10000;
+
+CREATE TABLE people (
+ id int NOT NULL,
+ first_name varchar(40) NOT NULL,
+ lock_version int default 0,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE people_id MINVALUE 10000;
+
+CREATE TABLE binaries (
+ id int NOT NULL,
+ data blob,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE binaries_id MINVALUE 10000;
+
+CREATE TABLE computers (
+ id int,
+ developer int NOT NULL,
+ PRIMARY KEY (id)
+);
+CREATE SEQUENCE computers_id MINVALUE 10000;
+
+EXIT;
View
7 activerecord/test/fixtures/db_definitions/oracle2.sql
@@ -0,0 +1,7 @@
+CREATE TABLE courses (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL
+);
+CREATE SEQUENCE courses_id MINVALUE 10000;
+
+EXIT;
View
5 activerecord/test/fixtures/topics.yml
@@ -3,8 +3,7 @@ first:
title: The First Topic
author_name: David
author_email_address: david@loudthinking.com
- written_on: 2003-07-16 15:28:00
- bonus_time: '12:13:14'
+ written_on: 2003-07-16t15:28:00.00+01:00
last_read: 2004-04-15
content: Have a nice day
approved: 0
@@ -14,7 +13,7 @@ second:
id: 2
title: The Second Topic's of the day
author_name: Mary
- written_on: 2003-07-15 15:28:00
+ written_on: 2003-07-15t15:28:00.00+01:00
content: Have a nice day
approved: 1
replies_count: 2
Please sign in to comment.
Something went wrong with that request. Please try again.