Commits

p2 committed 0847812 Merge

Merge branch 'develop'

  • Participants
  • Parent commits 117a8c9, fb225c0

Comments (0)

Files changed (10)

Classes/SQLKStructure.m

 						return NO;
 					}
 				}
+				
+				// TODO: Add support for renaming tables (have an "oldnames" property in the XML?)
+				// TODO: Add support for dropping tables
 			}
 		}
 		

Classes/SQLKTableStructure.m

 				// column is missing, add it
 				if (!existing) {
 					
-					/// @todo New columns may not: ( http://www.sqlite.org/lang_altertable.html )
+					// TODO: New columns may not: ( http://www.sqlite.org/lang_altertable.html )
 					//		- be PRIMARY
 					//		- be UNIQUE
 					//		- have CURRENT_[TIME|DATE|TIMESTAMP] as default
 						return NO;
 					}
 				}
+				
+				// TODO: To drop columns, we have to recreate the table without the column to be dropped
 			}
 		}
 		
 * An SQLiteObject, distantly related to NSManagedObject for CoreData
 * SQLK structures, easing database generation and updating from XML files
 
-The kit tries to complement **[fmdb][]**, the awesome SQLite Cocoa wrapper by Gus Mueller.
+The kit tries to complement [fmdb], the awesome SQLite Cocoa wrapper by Gus Mueller. The project uses ARC, so if you haven't yet moved to ARC you're on your own.
 
 [fmdb]: https://github.com/ccgus/fmdb
 
 Using SQLiteObject
 ------------------
 
-...
+For each table that you have, you create a subclass of SQLiteObject. You need to do at lest four things:
+
+* Create a subclass
+* Add properties
+* Override `tableName`
+* Override `tableKey`
+
+
+### Adding Properties ###
+
+You can add as many properties to the subclass as you'd like. To have the object recognize a property as one that is stored in the database, you synthesize it to **have an underscore at the end**, for example:
+
+    @synthesize db_prop = db_prop_;
+
+This makes the object automatically write and read this property from the database on `hydrate:` and `dehydrate`.
+
+
+### Overriding tableName and tableKey ###
+
+You override these class methods to tell the objects into which tables it belongs, like so:
+
+    + (NSString *)tableName
+    {
+    	return @"test_table";
+    }
+    
+    + (NSString *)tableKey
+    {
+    	return @"row_id";
+    }
+
+
+### Using SQLiteObject ###
+
+After these three steps you can now use your objects easily:
+
+#### Writing to the database ####
+
+```
+FMDatabase *db = [FMDatabase databaseWithPath:path-to-sqlite];
+MyObject *obj = [MyObject newWithDatabase:db];
+obj.db_prop = @"Hello World";
+
+NSError *error = nil
+if (![db dehydrate:&error]) {
+    NSLog(@"Dehydrate failed: %@", [error localizedDescription]);
+}
+```
+
+#### Reading from the database ####
+
+```
+FMDatabase *db = [FMDatabase databaseWithPath:path-to-sqlite];
+MyObject *obj = [MyObject newWithDatabase:db];
+obj.object_id = @1;
+
+if (![db hydrate]) {
+    NSLog(@"Hydrate failed");
+}
+```
 
 
 Using the kit
 -------------
 
-...
+The rest of the kit provides ways to create and update your SQLite database. If you want to use this functionality you can either use it as a subproject in your Xcode workspace or add the files like they were your own. Then link your app with:
+
+* libsqlite3.dylib
+* libsqlk.a
+
+The kit can read database structures from an XML file and create a database that represents this schema, and even update existing databases to match the schema, within the constraints of SQLite. Remember, SQLite can not rename or delete table columns.
+
+
+### XML Schema ###
+
+Here's an example schema:
+
+```
+<database>
+    <table name="objects">
+        <column name="object_id" type="varchar" primary="1" />
+        <column name="type" type="varchar" />
+        <column name="title" type="varchar" default="None" quote_default="1" />
+        <column name="year" type="int" />
+        <column name="lastedit" type="timestamp" default="CURRENT_TIMESTAMP" />
+        <constraint>UNIQUE (title, year) ON CONFLICT REPLACE</constraint>
+    </table>
+</database>
+```
+
+#### database ####
+
+The XML root object, attributes are not parsed.
+
+Children:
+
+* table
+
+#### table ####
+
+Describes one table.
+
+Attributes:
+
+* `name` _mandatory_, any valid SQLite table name
+* `old_names` _potentially_. **NOT IMPLEMENTED**; could be a good way to rename tables
+
+Children:
+
+* column
+* constraint
+
+#### column ####
+
+Describes one table column, doesn't take child nodes.
+
+Attributes:
+
+* `name` _mandatory_, any valid SQLite column name
+* `type` _mandatory_, any valid SQLite data type
+* `primary` _optional_, a **bool** indicating whether this is the primary key
+* `unique` _optional_, a **bool** indicating whether this column should be unique
+* `default` _optional_, the column's default value
+* `quote_default` _optional_, a **bool** indicating whether the value in `default` needs to be put in quotes (i.e. is a string, not a SQLite variable)
+
+#### constraint ####
+
+Describes a table constraint. The node takes no attributes and the node content should be a valid SQLite constraint.
 
-- libsqlite3.dylib
 
 
 To do
 -----
 
-### Better column parsing ###
+A lot! If you like the project and want to help out, fork, fix and send me pull requests.
+
+
+### SQLiteObject ###
+
+- Support fetching sub-properties of many objects, e.g. for listing purposes. I currently do that with a `+listQuery` object that only fetches a few properties, having this built-in would be nice.
+
+
+### XML ###
+
+- Write an actual XSD
+- Support more SQLite features
+
+
+### Better column parsing (from .sqlite) ###
 
 - Correctly parse DEFAULT values (currently regards everything after DEFAULT until the next comma as default)
 - Parse ON CONFLICT statements for UNIQUE and PRIMARY KEY statements (are currently ignored)

Unit Testing (iOS)/SQLKTestObject.m

 
 @implementation SQLKTestObject
 
-@synthesize db_string = _db_string_;
-@synthesize db_number = _db_number_;
+@synthesize db_string = db_string_;
+@synthesize db_number = db_number_;
 @synthesize non_db_string = _non_db_string;
 
++ (NSString *)tableName
+{
+	return @"test_table";
+}
+
++ (NSString *)tableKey
+{
+	return @"row_id";
+}
+
+
 @end

Unit Testing (iOS)/UnitTesting_iOS.m

 }
 
 
-- (void)testObject
-{
-	STAssertTrue(2 == [[SQLKTestObject dbVariables] count], @"Incorrect number of database-based variables");
-	STAssertFalse(3 == [[SQLKTestObject dbVariables] count], @"Incorrect number of database-based variables");
-}
-
-
 - (void)testStructures
 {
-	NSString *xmlPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"database1" ofType:@"xml"];
-	STAssertNotNil(xmlPath, @"Did not find database1.xml");
-	SQLKStructure *structure = [SQLKStructure structureFromXML:xmlPath];
-	STAssertNotNil(structure, @"Structure from database1.xml failed");
+	SQLKStructure *structure = [self structureFromXMLNamed:@"database1"];
 	
 	// is database1 correct?
 	STAssertTrue([structure hasTable:@"test_table"], @"test_table does not exist");
 	STAssertTrue([[structure tableWithName:@"test_table"] hasColumnNamed:@"lastedit_by"], @"test_table does not have lastedit_by");
 	
 	// create it
-	NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
-	STAssertTrue(([libraryPaths count] > 0), @"Library directory not found");
-	NSString *dbPath = [[libraryPaths objectAtIndex:0] stringByAppendingPathComponent:@"database.sqlite"];
-	STAssertNotNil(dbPath, @"No library to write to");
-	
-	NSFileManager *fm = [NSFileManager new];
-	[fm removeItemAtPath:dbPath error:nil];				// will be removed at the end of this method, but this makes it easier to debug and no harm is done
-	BOOL didCreate = NO;
-	FMDatabase *db = [structure createDatabaseAt:dbPath useBundleDbIfMissing:nil wasMissing:&didCreate updateStructure:NO error:nil];
-	STAssertNotNil(db, @"Failed to create sqlite database");
-	STAssertTrue(didCreate, @"Thinks it did not create a database");
-	STAssertTrue([db open], @"Failed to open the database");
+	NSString *dbPath = nil;
+	FMDatabase *db = [self databaseFromStructure:structure path:&dbPath];
 	
 	// first row
-	STAssertTrue([db executeUpdate:@"INSERT INTO test_table (group_id, type_id, value) VALUES ('abc', 'def', 666)"], @"Failed to insert row: %@", [db.lastError localizedDescription]);
+	STAssertTrue([db executeUpdate:@"INSERT INTO test_table (group_id, type_id, db_number) VALUES ('abc', 'def', 666)"], @"Failed to insert row: %@", [db.lastError localizedDescription]);
 	FMResultSet *res = [db executeQuery:@"SELECT * FROM test_table WHERE row_id = 1"];
 	STAssertNotNil(res, @"No first row");
 	[res next];
 	STAssertEqualObjects(@"abc", [res stringForColumn:@"group_id"], @"Wrong group_id");
-	STAssertEquals(666, [res intForColumn:@"value"], @"Wrong value");
+	STAssertEquals(666, [res intForColumn:@"db_number"], @"Wrong db_number");
 	[res close];
 	
 	// trigger constraint
-	STAssertTrue([db executeUpdate:@"INSERT INTO test_table (group_id, type_id, value) VALUES ('abc', 'def', 777)"], @"Failed to insert row: %@", [db.lastError localizedDescription]);
+	STAssertTrue([db executeUpdate:@"INSERT INTO test_table (group_id, type_id, db_number) VALUES ('abc', 'def', 777)"], @"Failed to insert row: %@", [db.lastError localizedDescription]);
 	res = [db executeQuery:@"SELECT * FROM test_table WHERE row_id = 1"];
 	STAssertFalse([res next], @"Row 1 is still present");
 	res = [db executeQuery:@"SELECT * FROM test_table WHERE row_id = 2"];
 	STAssertNotNil(res, @"No new first row");
 	[res next];
 	STAssertEqualObjects(@"def", [res stringForColumn:@"type_id"], @"Wrong type_id");
-	STAssertEquals(777, [res intForColumn:@"value"], @"Wrong value");
+	STAssertEquals(777, [res intForColumn:@"db_number"], @"Wrong db_number");
 	[res close];
 	
 	// compare to database2 (new column and a new table)
-	NSString *xmlPath2 = [[NSBundle bundleForClass:[self class]] pathForResource:@"database2" ofType:@"xml"];
-	STAssertNotNil(xmlPath2, @"Did not find database2.xml");
-	SQLKStructure *structure2 = [SQLKStructure structureFromXML:xmlPath2];
-	STAssertNotNil(structure2, @"Structure from database2.xml failed");
+	SQLKStructure *structure2 = [self structureFromXMLNamed:@"database2"];
 	STAssertFalse([structure2 isEqualTo:structure error:nil], @"Structure thinks it's the same as the old one");
 	
 	// update structure
-	STAssertTrue([structure2 updateDatabaseAt:dbPath dropTables:NO error:nil], @"Failed to update db structure");
-	STAssertTrue([db executeUpdate:@"UPDATE test_table SET description = \"Oh hell yes\" WHERE row_id = 2"], @"Failed to insert description");
-	res = [db executeQuery:@"SELECT description FROM test_table WHERE row_id = 2"];
+	NSError *error = nil;
+	STAssertTrue([structure2 updateDatabaseAt:dbPath dropTables:NO error:&error], @"Failed to update db structure: %@", [error localizedDescription]);
+	STAssertTrue([db executeUpdate:@"UPDATE test_table SET db_string = \"Oh hell yes\" WHERE row_id = 2"], @"Failed to insert db_string");
+	
+	// select an item
+	res = [db executeQuery:@"SELECT db_string FROM test_table WHERE row_id = 2"];
 	[res next];
-	STAssertEqualObjects(@"Oh hell yes", [res stringForColumnIndex:0], @"Wrong description");
+	STAssertEqualObjects(@"Oh hell yes", [res stringForColumnIndex:0], @"Wrong db_string");
 	[res close];
 	
 	// clean up
 	[db close];
-	NSLog(@"-->  %@", dbPath);
-//	[fm removeItemAtPath:dbPath error:nil];
+	//NSLog(@"-->  %@", dbPath);
+}
+
+
+- (void)testObject
+{
+	// SQLKTestObject has 2 database variables
+	STAssertTrue(2 == [[SQLKTestObject dbVariables] count], @"Incorrect number of database-based variables");
+	STAssertFalse(3 == [[SQLKTestObject dbVariables] count], @"Incorrect number of database-based variables");
+}
+
+
+- (void)testHydration
+{
+	SQLKStructure *structure = [self structureFromXMLNamed:@"database2"];
+	FMDatabase *db = [self databaseFromStructure:structure path:nil];
+	
+	// create a new object
+	SQLKTestObject *row1 = [SQLKTestObject newWithDatabase:db];
+	STAssertNotNil(row1, @"Failed to init new object");
+	row1.db_number = @28;
+	row1.db_string = @"Why so serious?";
+	
+	// dehydrate
+	NSError *error = nil;
+	STAssertTrue([row1 dehydrate:&error], @"Failed to dehydrate: %@", [error localizedDescription]);
+	
+	// update one value
+	NSString *theForce = @"The force is with you, always.";
+	row1.db_number = @3;
+	row1.db_string = theForce;
+	STAssertTrue([row1 dehydratePropertiesNamed:[NSSet setWithObject:@"db_string"] error:&error], @"Failed to update db_string: %@", [error localizedDescription]);
+	
+	// check if we really only updated the string
+	FMResultSet *res = [db executeQuery:@"SELECT db_number FROM test_table WHERE row_id = 1"];
+	STAssertTrue([res next], @"Failed to execute manual query");
+	STAssertEqualObjects(@28, [res objectForColumnIndex:0], @"Should have gotten 28, but got %@", [res objectForColumnIndex:0]);
+	
+	// hydrate new
+	SQLKTestObject *row = [SQLKTestObject newWithDatabase:db];
+	STAssertFalse([row hydrate], @"Hydrated despite not having an id");
+	row.object_id = @1;
+	STAssertTrue([row hydrate], @"Failed to hydrate");
+	STAssertEqualObjects(@28, row.db_number, @"Should have gotten 28, but got %@", row.db_number);
+	STAssertEqualObjects(theForce, row.db_string, @"Should have gotten \"%@\", but got %@", theForce, row.db_string);
+}
+
+
+
+#pragma mark - Utilities
+/**
+ *  Creates a structure from the given XML.
+ *  @return A structure created from the given XML
+ */
+- (SQLKStructure *)structureFromXMLNamed:(NSString *)xmlName
+{
+	// get the XML
+	NSString *xmlPath = [[NSBundle bundleForClass:[self class]] pathForResource:xmlName ofType:@"xml"];
+	STAssertNotNil(xmlPath, @"Did not find %@.xml", xmlName);
+	SQLKStructure *structure = [SQLKStructure structureFromXML:xmlPath];
+	STAssertNotNil(structure, @"Structure from %@.xml failed", xmlName);
+	
+	return structure;
+}
+
+/**
+ *  Creates a database from the given structure.
+ *  @return A database handle to the database created from the given XML
+ */
+- (FMDatabase *)databaseFromStructure:(SQLKStructure *)structure path:(NSString * __autoreleasing *)dbPath
+{
+	// get the cache directory
+	NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+	STAssertTrue(([libraryPaths count] > 0), @"Cache directory not found");
+	NSString *path = [[libraryPaths objectAtIndex:0] stringByAppendingPathComponent:@"database.sqlite"];
+	STAssertNotNil(path, @"No file to write to");
+	
+	// create it
+	NSFileManager *fm = [NSFileManager new];
+	[fm removeItemAtPath:path error:nil];				// we always want to start blank
+	BOOL didCreate = NO;
+	FMDatabase *db = [structure createDatabaseAt:path useBundleDbIfMissing:nil wasMissing:&didCreate updateStructure:NO error:nil];
+	STAssertNotNil(db, @"Failed to create sqlite database");
+	STAssertTrue(didCreate, @"Thinks it did not create a database");
+	STAssertTrue([db open], @"Failed to open the database");
+	
+	// return
+	if (dbPath) {
+		*dbPath = path;
+	}
+	return db;
 }
 
 

Unit Testing (iOS)/database1.xml

 		<column name="row_id" type="int" primary="1" />
 		<column name="group_id" type="varchar(36)" />
 		<column name="type_id" type="varchar(36)" />
-		<column name="value" type="int(7)" />
+		<column name="db_number" type="int(7)" />
 		<column name="lastedit" type="timestamp" default="CURRENT_TIMESTAMP" />
 		<column name="lastedit_by" type="int(6)" />
 		<column name="added" type="timestamp" default="CURRENT_TIMESTAMP" />

Unit Testing (iOS)/database2.xml

 		<column name="row_id" type="int" primary="1" />
 		<column name="group_id" type="varchar(36)" />
 		<column name="type_id" type="varchar(36)" />
-		<column name="value" type="int" />
-		<column name="description" type="text" />
+		<column name="db_number" type="int" />
+		<column name="db_string" type="text" />
 		<column name="lastedit" type="timestamp" default="CURRENT_TIMESTAMP" />
 		<column name="lastedit_by" type="int" />
 		<column name="added" type="timestamp" default="CURRENT_TIMESTAMP" />

Unit Testing (iOS)/en.lproj/InfoPlist.strings

-/* Localized versions of Info.plist keys */
-

sqlk.xcodeproj/project.pbxproj

 		EE08DE6215A9E07E00CF6086 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE08DE6115A9E07E00CF6086 /* SenTestingKit.framework */; };
 		EE08DE6415A9E07E00CF6086 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE08DE6315A9E07E00CF6086 /* UIKit.framework */; };
 		EE08DE6515A9E07E00CF6086 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AACBBE490F95108600F1A2B1 /* Foundation.framework */; };
-		EE08DE6B15A9E07E00CF6086 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EE08DE6915A9E07E00CF6086 /* InfoPlist.strings */; };
 		EE08DE6E15A9E07E00CF6086 /* UnitTesting_iOS.m in Sources */ = {isa = PBXBuildFile; fileRef = EE08DE6D15A9E07E00CF6086 /* UnitTesting_iOS.m */; };
 		EE08DE8215A9E28500CF6086 /* libsqlk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AAC07E0554694100DB518D /* libsqlk.a */; };
 		EE08DE8415A9E28500CF6086 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = EE08DE8315A9E28500CF6086 /* libsqlite3.dylib */; };
 		EE08DE6115A9E07E00CF6086 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; };
 		EE08DE6315A9E07E00CF6086 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
 		EE08DE6815A9E07E00CF6086 /* Unit Testing (iOS)-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Unit Testing (iOS)-Info.plist"; sourceTree = "<group>"; };
-		EE08DE6A15A9E07E00CF6086 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		EE08DE6C15A9E07E00CF6086 /* UnitTesting_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnitTesting_iOS.h; sourceTree = "<group>"; };
 		EE08DE6D15A9E07E00CF6086 /* UnitTesting_iOS.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnitTesting_iOS.m; sourceTree = "<group>"; };
 		EE08DE6F15A9E07E00CF6086 /* Unit Testing (iOS)-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Unit Testing (iOS)-Prefix.pch"; sourceTree = "<group>"; };
 			children = (
 				EE08DE7A15A9E24E00CF6086 /* README.md */,
 				EE51873B146EFAA6000BB0FC /* Code */,
-				32C88DFF0371C24200C91783 /* Other Sources */,
 				EEE49872133AB93800FEDD45 /* SQLiteKit */,
 				EE08DE6615A9E07E00CF6086 /* Unit Testing (iOS) */,
+				32C88DFF0371C24200C91783 /* Other Sources */,
 				0867D69AFE84028FC02AAC07 /* Frameworks */,
 				034768DFFF38A50411DB9C8B /* Products */,
 			);
 			isa = PBXGroup;
 			children = (
 				EE08DE6815A9E07E00CF6086 /* Unit Testing (iOS)-Info.plist */,
-				EE08DE6915A9E07E00CF6086 /* InfoPlist.strings */,
 				EE08DE6F15A9E07E00CF6086 /* Unit Testing (iOS)-Prefix.pch */,
 			);
 			name = "Supporting Files";
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				EE08DE6B15A9E07E00CF6086 /* InfoPlist.strings in Resources */,
 				EE08DE8915A9E4A700CF6086 /* database1.xml in Resources */,
 				EE08DE9215A9EE2100CF6086 /* database2.xml in Resources */,
 			);
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
-		EE08DE6915A9E07E00CF6086 /* InfoPlist.strings */ = {
-			isa = PBXVariantGroup;
-			children = (
-				EE08DE6A15A9E07E00CF6086 /* en */,
-			);
-			name = InfoPlist.strings;
-			sourceTree = "<group>";
-		};
 		EEE49875133AB93800FEDD45 /* InfoPlist.strings */ = {
 			isa = PBXVariantGroup;
 			children = (
 				ARCHS = "$(ARCHS_STANDARD_32_BIT)";
 				COPY_PHASE_STRIP = NO;
 				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_MODEL_TUNING = G5;
 				GCC_OPTIMIZATION_LEVEL = 0;
 				GCC_PRECOMPILE_PREFIX_HEADER = YES;
 				GCC_PREFIX_HEADER = sqlk_Prefix.pch;
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ARCHS = "$(ARCHS_STANDARD_32_BIT)";
-				GCC_MODEL_TUNING = G5;
 				GCC_PRECOMPILE_PREFIX_HEADER = YES;
 				GCC_PREFIX_HEADER = sqlk_Prefix.pch;
 				PRODUCT_NAME = sqlk;
 				GCC_DYNAMIC_NO_PIC = NO;
 				GCC_PRECOMPILE_PREFIX_HEADER = YES;
 				GCC_PREFIX_HEADER = "Unit Testing (iOS)/Unit Testing (iOS)-Prefix.pch";
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
 				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES;
 				INFOPLIST_FILE = "Unit Testing (iOS)/Unit Testing (iOS)-Info.plist";
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ARCHS = "$(ARCHS_STANDARD_32_BIT)";
-				GCC_MODEL_TUNING = G5;
 				GCC_PRECOMPILE_PREFIX_HEADER = YES;
 				GCC_PREFIX_HEADER = sqlk_Prefix.pch;
 				PRODUCT_NAME = sqlk;