ramen 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. data/doc/doc_resources/MetaData_Class_Diagram.gif +0 -0
  2. data/doc/doc_resources/MetaData_Class_Diagram.png +0 -0
  3. data/doc/doc_resources/Thumbs.db +0 -0
  4. data/lib/ramen/core.rb +75 -0
  5. data/lib/ramen/default_logger.rb +29 -0
  6. data/lib/ramen/engine/engine.rb +34 -0
  7. data/lib/ramen/engine/mysql.rb +225 -0
  8. data/lib/ramen/engine/sql2005.rb +377 -0
  9. data/lib/ramen/home.rb +162 -0
  10. data/lib/ramen/metadata/column.rb +38 -0
  11. data/lib/ramen/metadata/database.rb +137 -0
  12. data/lib/ramen/metadata/foreign_key.rb +71 -0
  13. data/lib/ramen/metadata/foreign_key_column.rb +51 -0
  14. data/lib/ramen/metadata/index.rb +71 -0
  15. data/lib/ramen/metadata/index_column.rb +49 -0
  16. data/lib/ramen/metadata/key_constraint.rb +56 -0
  17. data/lib/ramen/metadata/metadata.rb +10 -0
  18. data/lib/ramen/metadata/primary_key.rb +71 -0
  19. data/lib/ramen/metadata/primary_key_column.rb +49 -0
  20. data/lib/ramen/metadata/schema.rb +86 -0
  21. data/lib/ramen/metadata/table.rb +202 -0
  22. data/lib/ramen/ramen_error.rb +16 -0
  23. data/lib/ramen/ramen_hash.rb +143 -0
  24. data/lib/ramen/ramen_module.rb +52 -0
  25. data/lib/ramen/row_data_gateway.rb +89 -0
  26. data/lib/ramen/version.rb +16 -0
  27. data/lib/ramen.rb +62 -0
  28. data/readme.txt +159 -0
  29. data/test/config/config.yml +19 -0
  30. data/test/helper.rb +18 -0
  31. data/test/mysql_create_test_db.sql +47 -0
  32. data/test/sql2005_create_test_db.sql +45 -0
  33. data/test/test_bugs.rb +32 -0
  34. data/test/test_core.rb +63 -0
  35. data/test/test_default_logger.rb +38 -0
  36. data/test/test_home.rb +33 -0
  37. data/test/test_ramen.rb +274 -0
  38. data/test/test_ramen_hash.rb +78 -0
  39. data/test/test_ramen_module.rb +21 -0
  40. metadata +99 -0
data/readme.txt ADDED
@@ -0,0 +1,159 @@
1
+ = Ramen -- Making your database edible
2
+ by Gregory N. Houston
3
+
4
+ Ramen creates an object model from your database's metadata (Schema, Tables,
5
+ Columns, Indexes, Keys). This metadata is sometimes called
6
+ _data_ _dictionary_, _system_ _catalog_, or _INFORMATION_ _SCHEMA_.
7
+
8
+ Version:: 0.4.0 SQL Server 2005 and MySQL 5.0.
9
+
10
+ It is easy to support additional database engines. All that is needed are
11
+ the SQL statements to query the metadata, and maybe a few tweaks to the
12
+ unit tests. Please contact the author if you want help getting Ramen to
13
+ support another database.
14
+
15
+ == Links:
16
+
17
+ Start Here:: http://ramen.rubyforge.org
18
+
19
+ Project:: http://rubyforge.org/projects/ramen
20
+ Documents:: http://ramen.rubyforge.org
21
+ RubyGems:: Install with: <b>gem install ramen</b>
22
+ Download:: Download from RubyForge at http://rubyforge.org/projects/ramen/
23
+ Authors Blog:: http://ghouston.blogspot.com
24
+ Browse Source:: link:rcov/index.html
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License + Free Software Foundation Advertising Prohibition)
29
+
30
+ Copyright (c) 2007 Gregory N. Houston
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining a copy
33
+ of this software and associated documentation files (the "Software"), to deal
34
+ in the Software without restriction, including without limitation the rights
35
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
36
+ copies of the Software, and to permit persons to whom the Software is
37
+ furnished to do so, subject to the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be included in
40
+ all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
48
+ THE SOFTWARE.
49
+
50
+ Except as contained in this notice, the name(s) of the above copyright holders
51
+ shall not be used in advertising or otherwise to promote the sale, use or other
52
+ dealings in this Software without prior written authorization.
53
+
54
+ == Examples:
55
+
56
+ == Design:
57
+
58
+ Ramen loads the INFORMATION SCHEMA from the database into Ruby objects
59
+ using the RowDataGateway pattern (Martin Fowler. Patterns of Enterprise
60
+ Application Architecture. Addison Wesley Longman, Reading, MA, 2003). The
61
+ resulting structure is:
62
+
63
+ link:doc_resources/MetaData_Class_Diagram.png
64
+
65
+ Thus if a client wishes to get the Employee table in the HumanResources schema:
66
+
67
+ require 'ramen'
68
+ db = Ramen.create( :connection => DBI.connect( ...database connection... ))
69
+ employee_table = db.schema['HumanResources'].table['Employee']
70
+
71
+ Ramen exposes each column in the INFORMATION SCHEMA as attributes on the Ruby
72
+ object. For example, on SQL 2005 a table in sys.table has the following
73
+ meta-data:
74
+
75
+ select * from sys.tables where name ='Employee'
76
+
77
+ ...type_desc create_date modify_date is_ms_shipped ...
78
+ ...----------- ----------------------- ----------------------- ------------- ...
79
+ ...USER_TABLE 2005-10-14 01:58:32.663 2005-10-14 01:59:52.287 0 ...
80
+
81
+ Ramen's RowDataGateway maps these columns directly to attributes on the table
82
+ object in ruby.
83
+
84
+ employee_table.type_desc #=> "USER_TABLE"
85
+ employee_table.create_date #=> #<DBI::Timestamp...>
86
+ employee_table.is_ms_shipped #=> 0
87
+
88
+ The attributes are database engine specific. On different database engines
89
+ (Sql 2005, MySQL, etc) the objects will contain the data columns
90
+ returned by the engine's INFORMATION SCHEMA. In the near future, Ramen will
91
+ provide a Facade mapping engine specific attributes to generic attributes. For
92
+ example in SQL 2005 the Index attribute .is_unique and MySQL's .non_unique
93
+ attributes would be mapped to .unique?.
94
+
95
+ Ramen does not "Rubify" the database names into Ruby names. This is important
96
+ since Ramen's focus is the database, not Ruby. The only restriction that Ramen
97
+ imposes is the "name" and "object_id" or "id" columns need renaming to names
98
+ like "schema_name" or "table_id" in the records returned in queries. This
99
+ avoids clashing with attributes that already exist on Ruby objects, and
100
+ it makes it clear which ID or NAME is being returned.
101
+
102
+ Since different databases use
103
+ Internally Ramen collects objects into a specialized Hash that can access
104
+ objects by either name or id. For example, when getting a table a client
105
+ can use either table_name or table_id.
106
+
107
+ hr_schema.table['Employee']
108
+ hr_schema.table[12345]
109
+
110
+ Ramen was developed to support the Noodle project (also on Rubyforge). As
111
+ Noodle is developed, Ramen's API will improve.
112
+
113
+ == Database Support:
114
+
115
+ Currently, Ramen only supports SQL Server 2005 and MySQL 5.0. However, Ramen was developed
116
+ with support for other databases in mind. As such, to support other databases
117
+ two changes are needed. First, the queries for the INFORMATION SCHEMA meta-data
118
+ would need writting. See the class Ramen::Sql2005 for the queries that would need
119
+ developing for another database. Second, the unit tests will need tweaking to
120
+ work with the new database engine (see test/sql2005_create_test_db.sql).
121
+
122
+ == Multiple Databases:
123
+
124
+ Ramen can create and use multiple instances of Ramen::Database at the same time.
125
+ This allow connecting to multiple databases at the same time.
126
+
127
+ == Connections:
128
+
129
+ Ramen::Database#new and Ramen.create accept a connection parameter. This
130
+ connection is only used during the call of the method. Ramen will not
131
+ hold on to connections. After the method returns, it is safe to use the
132
+ connection outside Ramen or to close it.
133
+
134
+ == Threading:
135
+
136
+ Ramen was developed to support Noodle and Unit Testing in a single-threaded
137
+ fashion. Thus Ramen does not take extra steps to make itself threadsafe. The
138
+ only section of Ramen's code that updates values is the creation process.
139
+ Once the database meta-data is loaded, Ramen's object are read-only
140
+ (not enforced, but as long as clients only use methods documented as
141
+ "for external use" it is safe). It is possible to use Ramen in a multi-threaded
142
+ system if Ramen::Database#new is treated as a critical section.
143
+
144
+ == Dependencies:
145
+
146
+ Ramen's connection must quack like DBI.
147
+
148
+ Ramen's logger must quack like Ramen::DefaultLogger which is a subset of Log4r.
149
+ So clients may use Ramen directly with Log4r.
150
+
151
+ == Performance:
152
+
153
+ Ramen was optimized for Unit Testing. It assumes that most clients will
154
+ request meta-data for at least 25% of the tables in the database. To optimize
155
+ for this situation, Ramen found loading the entire database's meta-data upon
156
+ creation of the Database object to have the best performance. In development
157
+ tests on an average PC running Ramen against a remote SQL 2005 Server database on
158
+ the same network segment that contained 322 tables, 3281 columns, 192 foreign
159
+ keys, the queries and creation of Ramen's objects took less than one second.
@@ -0,0 +1,19 @@
1
+ ## config.yml - Configuration for running the tests
2
+ ## This is an example file, which contains the
3
+ ## default settings.
4
+ ## Overridden by defining a config_local.yml
5
+ ##
6
+ ## The top element is an environment name, which will be printed in
7
+ ## error messages when a test against the environment fails
8
+ ##
9
+ ## Each environment needs an 'engine' and 'connection'
10
+ ## engine = module name that contains engine's specific code.
11
+ ## connection = DBI connection string, to connect to test database.
12
+
13
+ sqlserver2005:
14
+ engine: Ramen::Sql2005
15
+ connection: DBI:ODBC:ramen_test_sql2005
16
+
17
+ mysql50:
18
+ engine: Ramen::MySql500
19
+ connection: DBI:ODBC:ramen_test_mysql
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ #--
2
+ # Copyright (c) 2007 Gregory N. Houston
3
+ # See ramen.rb for license information.
4
+ #++
5
+
6
+ module Ramen
7
+ class Helper
8
+ def Helper.load_config
9
+ begin
10
+ return YAML.load_file( './test/config/config_local.yml' )
11
+ rescue Errno::ENOENT
12
+ return YAML.load_file( './test/config/config.yml' )
13
+ end
14
+ raise "Unable to load configuration. #{__FILE__} #{__LINE__}"
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,47 @@
1
+ DROP DATABASE IF EXISTS RamenTestSchema;
2
+ DROP DATABASE IF EXISTS RamenTestSchema2;
3
+
4
+ CREATE DATABASE RamenTestSchema;
5
+ CREATE DATABASE RamenTestSchema2;
6
+
7
+ CREATE TABLE RamenTestSchema.Employee (
8
+ EmployeeID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
9
+ LoginID VARCHAR(256) NOT NULL,
10
+ ManagerID INTEGER UNSIGNED,
11
+ PRIMARY KEY (EmployeeID)
12
+ );
13
+ ALTER TABLE RamenTestSchema.Employee ADD UNIQUE INDEX IX_Employee_LoginID USING BTREE(LoginID);
14
+ ALTER TABLE RamenTestSchema.Employee ADD INDEX IX_Employee_ManagerID USING BTREE(ManagerID);
15
+ ALTER TABLE RamenTestSchema.Employee
16
+ ADD CONSTRAINT FK_Employee_Employee_ManagerID
17
+ FOREIGN KEY FK_Employee_Employee_ManagerID (ManagerID)
18
+ REFERENCES Employee (EmployeeID)
19
+ ON DELETE RESTRICT
20
+ ON UPDATE RESTRICT;
21
+
22
+ CREATE TABLE RamenTestSchema.EmployeeAddress (
23
+ EmployeeID INTEGER UNSIGNED NOT NULL,
24
+ AddressID INTEGER UNSIGNED NOT NULL,
25
+ PRIMARY KEY (EmployeeID, AddressID)
26
+ );
27
+ ALTER TABLE RamenTestSchema.EmployeeAddress
28
+ ADD CONSTRAINT FK_EmployeeAddress_Employee
29
+ FOREIGN KEY FK_EmployeeAddress_Employee (EmployeeID)
30
+ REFERENCES Employee (EmployeeID)
31
+ ON DELETE RESTRICT
32
+ ON UPDATE RESTRICT;
33
+
34
+ CREATE TABLE RamenTestSchema.Bug1 (
35
+ PkID INTEGER UNSIGNED NOT NULL,
36
+ PRIMARY KEY (PkID)
37
+ );
38
+ ALTER TABLE RamenTestSchema.Bug1
39
+ ADD CONSTRAINT FK_Bug1_Employee
40
+ FOREIGN KEY FK_Bug1_Employee (PkID)
41
+ REFERENCES Employee (EmployeeID)
42
+ ON DELETE RESTRICT
43
+ ON UPDATE RESTRICT;
44
+ /*
45
+ # Note, MySQL cant have a foreign key that spans schemas (databases).
46
+ # Some databases dont have this restriction.
47
+ */
@@ -0,0 +1,45 @@
1
+ /*
2
+ #--
3
+ # Copyright (c) 2007 Gregory N. Houston
4
+ # See ramen.rb for license information.
5
+ #++
6
+ */
7
+
8
+ USE MASTER
9
+ GO
10
+
11
+ IF EXISTS (SELECT name FROM sys.databases WHERE name = N'RamenTest')
12
+ DROP DATABASE RamenTest
13
+ GO
14
+
15
+ CREATE DATABASE RamenTest
16
+ GO
17
+
18
+ USE RamenTest
19
+ GO
20
+
21
+ CREATE SCHEMA RamenTestSchema2
22
+ CREATE TABLE Bug1 (PkID int
23
+ CONSTRAINT PK_Bug1_PkID PRIMARY KEY CLUSTERED (PkID ASC))
24
+ GO
25
+
26
+ CREATE SCHEMA RamenTestSchema
27
+ CREATE TABLE Employee (EmployeeID int, LoginID nvarchar(256), ManagerID int
28
+ CONSTRAINT PK_Employee_EmployeeID PRIMARY KEY CLUSTERED (EmployeeID ASC))
29
+ CREATE TABLE EmployeeAddress (EmployeeID int, AddressID int
30
+ CONSTRAINT PK_EmployeeAddress_EmployeeID_AddressID PRIMARY KEY CLUSTERED (EmployeeID ASC, AddressID ASC))
31
+ GO
32
+
33
+ CREATE UNIQUE NONCLUSTERED INDEX IX_Employee_LoginID ON RamenTestSchema.Employee (LoginID ASC)
34
+ CREATE NONCLUSTERED INDEX IX_Employee_ManagerID ON RamenTestSchema.Employee (ManagerID ASC)
35
+ ALTER TABLE RamenTestSchema.Employee ADD CONSTRAINT FK_Employee_Employee_ManagerID FOREIGN KEY(ManagerID) REFERENCES RamenTestSchema.Employee (EmployeeID)
36
+ ALTER TABLE RamenTestSchema.Employee CHECK CONSTRAINT FK_Employee_Employee_ManagerID
37
+ ALTER TABLE RamenTestSchema.EmployeeAddress ADD CONSTRAINT FK_EmployeeAddress_Employee FOREIGN KEY(EmployeeID) REFERENCES RamenTestSchema.Employee (EmployeeID)
38
+ ALTER TABLE RamenTestSchema.EmployeeAddress CHECK CONSTRAINT FK_EmployeeAddress_Employee
39
+ GO
40
+
41
+ ALTER TABLE RamenTestSchema2.Bug1 ADD CONSTRAINT FK_Bug1_Employee FOREIGN KEY(PkID) REFERENCES RamenTestSchema.Employee (EmployeeID)
42
+ GO
43
+
44
+ --CREATE SCHEMA Person
45
+ --GO
data/test/test_bugs.rb ADDED
@@ -0,0 +1,32 @@
1
+ #--
2
+ # Copyright (c) 2007 Gregory N. Houston
3
+ # See ramen.rb for license information.
4
+ #++
5
+ require 'lib/ramen'
6
+
7
+ =begin
8
+ Detected: 2008-02-18
9
+ Database Engine: Sql2005
10
+ Bug: Foreign Key Column @referenced_column fails if the ref column name is
11
+ different than the column name in the local table.
12
+
13
+ To reproduce:
14
+ RamenTestSchema2.Bug1 with a column PkID which references
15
+ RamenTestSchema.Employee (EmployeeID).
16
+
17
+ Other problems: Inspecting the code, found that Foreign Key Column doesn't
18
+ have an attribute for referenced_table_schema. What if the reference was to
19
+ a different schema? (Note, MySQL cant reference across schemas)
20
+ =end
21
+
22
+ module Ramen
23
+ module TestBugs
24
+ def t_bug1( key, db )
25
+ @name = "test_bug1_#{key}"
26
+ table_schema = (db.schema['RamenTestSchema'].table.has_key?('bug1')) ? 'RamenTestSchema' : 'RamenTestSchema2'
27
+ referenced_column = db.schema[table_schema].table['bug1'].fk['fk_bug1_employee'].column['pkid'].referenced_column
28
+ employee_id_column = db.schema['RamenTestSchema'].table['employee'].column['employeeid']
29
+ assert_same( referenced_column, employee_id_column, "referenced column must be employee id column in employee table." )
30
+ end
31
+ end
32
+ end
data/test/test_core.rb ADDED
@@ -0,0 +1,63 @@
1
+ #--
2
+ # Copyright (c) 2007 Gregory N. Houston
3
+ # See ramen.rb for license information.
4
+ #++
5
+ require 'test/unit'
6
+ require 'lib/ramen'
7
+
8
+ class TestCore < Test::Unit::TestCase
9
+ def testIsUpcase
10
+ assert "ABCDEFG".is_upcase?
11
+ assert !"abcdefg".is_upcase?
12
+ assert !"Abcdefg".is_upcase?
13
+ end
14
+
15
+ def testToDelimitedWords
16
+ assert_equal "foo", "Foo".to_delimited_words
17
+ assert_equal "foo_bar_baz", "FooBarBaz".to_delimited_words
18
+ assert_equal "", "".to_delimited_words
19
+ end
20
+
21
+ def test_compare
22
+ assert_equal( -1, nil <=> 'foobar' )
23
+ assert_equal( nil, 'foobar' <=> nil )
24
+ assert_equal( 0, nil <=> nil )
25
+ end
26
+
27
+ def test_singleton_class
28
+ a = TestCore
29
+ b = TestCore
30
+ c = "another instance" #these two strings are different instances of String
31
+ d = "another instance"
32
+ e = "no foo"
33
+ a.singleton_class.module_eval do
34
+ define_method :foo do
35
+ "a"
36
+ end
37
+ end
38
+ b.singleton_class.module_eval do
39
+ define_method :foo do
40
+ "b"
41
+ end
42
+ end
43
+ c.singleton_class.module_eval do
44
+ define_method :foo do
45
+ "c"
46
+ end
47
+ end
48
+ d.singleton_class.module_eval do
49
+ define_method :foo do
50
+ "d"
51
+ end
52
+ end
53
+ assert_equal( a.singleton_class, b.singleton_class )
54
+ assert_equal( "b", a.foo )
55
+ assert_equal( "b", b.foo )
56
+ assert_not_equal( c.singleton_class, d.singleton_class )
57
+ assert_equal( "c", c.foo )
58
+ assert_equal( "d", d.foo )
59
+ assert( !(e.respond_to? :foo) )
60
+ end
61
+ end
62
+
63
+
@@ -0,0 +1,38 @@
1
+ #--
2
+ # Copyright (c) 2007 Gregory N. Houston
3
+ # See ramen.rb for license information.
4
+ #++
5
+
6
+ require 'test/unit'
7
+ require 'lib/ramen'
8
+
9
+ module Ramen
10
+ # simple test
11
+ #
12
+ # ensures the logger interface (actually this was added for code coverage)
13
+ class TestDefaultLogger < Test::Unit::TestCase
14
+ def test_log
15
+ capture_stdout
16
+ logger = DefaultLogger.new
17
+ logger.debug 'debug'
18
+ logger.info 'info'
19
+ logger.warn 'warn'
20
+ logger.error 'error'
21
+ logger.fatal 'fatal'
22
+ result = restore_stdout
23
+ assert_equal "warn\nerror\nfatal\n", result
24
+ end
25
+
26
+ def capture_stdout
27
+ @in, @out = IO.pipe
28
+ @stdout = $stdout
29
+ $stdout = @out
30
+ end
31
+
32
+ def restore_stdout
33
+ $stdout = @stdout
34
+ @out.close
35
+ return @in.read
36
+ end
37
+ end
38
+ end
data/test/test_home.rb ADDED
@@ -0,0 +1,33 @@
1
+ #--
2
+ # Copyright (c) 2007 Gregory N. Houston
3
+ # See ramen.rb for license information.
4
+ #++
5
+
6
+ require 'test/unit'
7
+ require 'lib/ramen'
8
+
9
+ module Ramen
10
+ class MockDBI
11
+ def execute( *args )
12
+ raise RamenError, "expected"
13
+ end
14
+ end
15
+ class SilentLogger
16
+ def do_nothing( msg )
17
+ end
18
+ alias debug do_nothing
19
+ alias info do_nothing
20
+ alias warn do_nothing
21
+ alias error do_nothing
22
+ alias fatal do_nothing
23
+ end
24
+ class TestHome < Test::Unit::TestCase
25
+ def test_dbi_error
26
+ h = Home.new( :logger => SilentLogger.new )
27
+ assert_raise RamenError do
28
+ h.execute( MockDBI.new, 'i dont care')
29
+ end
30
+ end
31
+ end
32
+ end
33
+