Reading and Writing Stream problems

BlitzMax Forums/BlitzMax Programming/Reading and Writing Stream problems

Fry Crayola(Posted 2005) [#1]
This one's been bugging me for a few days now, and I can't seem to locate the error.

The program is essentially a simple relational database wherein all the actual data is stored in a Bank field in the Table type, with two other types (FieldGroup and Field) used to hold offset information and fieldnames, etc.

However, my code to save and load the data to the hard drive for further use isn't working properly - the data is garbled somewhere along the line and thus becomes unusable. Whether or not it works depends on the data being used, and if it does work, saving after adding further data to the database will bring up the same error after loading.

Sorry if it's not too clear.

The save function is:

'Data Saving
Function SaveData()
	
	Local FileName:String = Input("Enter Filename: ")
	
	Local file:TStream = WriteStream(FileName)
		
	'save totals
	WriteInt(file, TotalTables)
	WriteInt(file, TotalFieldGroups)
	WriteInt(file, TotalFields)
	
	'save tables, then field groups, then fields
	For Local tbl:DBTable = EachIn DBTableList
		WriteLine(file, tbl.Name)
		WriteInt(file, tbl.Id)
		WriteInt(file, tbl.Records)
		WriteInt(file, tbl.MaxRecords)
		WriteInt(file, tbl.MaxSize)
		WriteInt(file, tbl.Mode)
		WriteInt(file, tbl.RecordSize)
		
		For Local a = 0 To tbl.MaxRecords - 1
			WriteInt(file, tbl.Offsets[a])
		Next
		
		'Bank Sizes
		WriteInt(file, BankSize(tbl.DataBank))
		WriteInt(file, BankSize(tbl.RecordFlags))
		
		WriteBank(tbl.DataBank, file, 0, BankSize(tbl.DataBank))
		WriteBank(tbl.RecordFlags, file, 0, BankSize(tbl.RecordFlags))
		
	Next
		
	For Local grp:DBFieldGroup = EachIn DBFieldGroupList
		WriteLine(file, grp.Name)
		WriteInt(file, grp.Id)
		WriteInt(file, grp.Size)
		WriteInt(file, grp.Table)
		WriteInt(file, grp.Offset)
	Next
		
	For Local fld:DBField = EachIn DBFieldList
		WriteLine(file, fld.Name)
		WriteLine(file, fld.StrValue)
		WriteLine(file, fld.DefaultStr)
		WriteInt(file, fld.Id)
		WriteInt(file, fld.DataType)
		WriteInt(file, fld.Size)
		WriteInt(file, fld.BitSize)
		WriteInt(file, fld.Offset)
		WriteInt(file, fld.Group)
		WriteInt(file, fld.Table)
		WriteInt(file, fld.IntValue)
		WriteInt(file, fld.DefaultInt)
		WriteInt(file, fld.Record)
	Next
		
	Print "Saved!"
	
	CloseStream file
	
End Function


The load function is

Function LoadData()
	
	For Local tbl:DBTable = EachIn DBTableList
		ListRemove(DBTableList, tbl)
	Next
	
	For Local grp:DBFieldGroup = EachIn DBFieldGroupList
		ListRemove(DBFieldGroupList, grp)
	Next
	
	For Local fld:DBField = EachIn DBFieldList
		ListRemove(DBFieldList, fld)
	Next
	
	Print "Existing Data is wiped!"
	
	
	
	
	Local FileName:String = Input("Enter Filename: ")
	
	Local file:TStream = ReadStream(FileName)
		
	'load in totals
	TotalTables = Readint(file)
	TotalFieldGroups = Readint(file)
	TotalFields = Readint(file)
	
	'load tables, then field groups, then fields
	
	For Local count = 1 To TotalTables
		Local tbl:DBTable = New DBTable
		tbl.Name = ReadLine(file)
		tbl.Id = Readint(file)
		tbl.Records = Readint(file)
		tbl.MaxRecords = Readint(file)
		tbl.MaxSize = Readint(file)
		tbl.Mode = Readint(file)
		tbl.RecordSize = Readint(file)
		
		tbl.Offsets = New Int[tbl.MaxRecords]
		For Local a = 0 To tbl.MaxRecords - 1
			tbl.Offsets[a] = Readint(file)
		Next
		
		Local DataBankSize = Readint(file)
		Local RecordFlagSize = Readint(file)
		
		tbl.DataBank = CreateBank(DataBankSize)
		tbl.RecordFlags = CreateBank(RecordFlagSize)
		
		ReadBank(tbl.DataBank, file, 0, DataBankSize)
		ReadBank(tbl.RecordFlags, file, 0, RecordFlagSize)
		
		ListAddLast(DBTableList, tbl)
	Next
	
	For Local count = 1 To TotalFieldGroups
		Local grp:DBFieldGroup = New DBFieldGroup
		grp.Name = ReadLine(file)
		grp.Id = Readint(file)
		grp.Size = Readint(file)
		grp.Table = Readint(file)
		grp.Offset = Readint(file)
		
		ListAddLast(DBFieldGroupList, grp)
	Next
	
	For Local count = 1 To TotalFields
		Local fld:DBField = New DBField
		fld.Name = ReadLine(file)
		fld.StrValue = ReadLine(file)
		fld.DefaultStr = ReadLine(file)
		fld.Id = Readint(file)
		fld.DataType = Readint(file)
		fld.Size = Readint(file)
		fld.BitSize = Readint(file)
		fld.Offset = Readint(file)
		fld.Group = Readint(file)
		fld.Table = Readint(file)
		fld.IntValue = Readint(file)
		fld.DefaultInt = Readint(file)
		fld.Record = Readint(file)
		
		ListAddLast(DBFieldList, fld)
	Next
	
	Print "Loaded!"
	
	CloseStream file 
	
End Function


I'm wondering if I'm doing something wrong with the way I'm saving and loading the data. I've scoured the code for hours trying to find if I'm forgetting to save an important piece of data, or if I'm correctly creating the Table, FieldGroup and Field types and adding them to their list (although I'm convinced that's not the problem, as the data read in is garbled before I add the object to the list).

Note that I've mainly only encountered a problem with the "Field" data - never with the FieldGroup or Table data (indeed, the banks are being read and stored correctly). The field data ends up with offsets in the tens of thousands, bitsizes far too big and results in garbled data. On one occasion, however, I did get a error reading a databank (trying to access a memory location out of range), but never had enough time to find the exact cause - it's likely part of the field offset problem.

Any help is greatly, greatly appreciated.


Scott Shaver(Posted 2005) [#2]
I don't see anything wrong right off the bat. You might try adding a number of debuglog statements to make sure the values you are writing are really what you think they are.

Another possible issue is that you are mixing binary and text mode commands. Don't use WriteLine and ReadLine, use WriteInt to write the length of the string data and then WriteString to write the char data. The when reading use ReadInt and ReadString. I'm not sure but the use of Write/Read Line might be throwing you off somewhere.

Does the problem alway happen? What data causes it to happen? What data doesn't cause it to happen?


Fry Crayola(Posted 2005) [#3]
I'll certainly give the WriteInt/WriteString combo a whirl, I might get lucky but you never know.

The problem doesn't always happen, but I've yet to properly isolate the cases in which it does occur. I have stepped through the code using the debugger, and the values I'm writing are the ones that should be written. When read, they're screwed up - but only for the Field types - the preceeding Tables and FieldGroups are read fine.

One case in which it definitely happens is as follows:

There are two objects of type Table, two of type FieldGroup, and four of type Field (meaning each table has one field group, with two fields in it). When created in one sitting, this saves and loads perfectly well.

When I then add another Table, with one Field Group and two Fields, then save and reload that (including when starting the program from scratch) the problem occurs - when I select an option to view the contents, the fields themselves are incorrectly read due to the screwed field data.


Scott Shaver(Posted 2005) [#4]
In that case we need to see the rest of your code. The entire type definitions for Table, FieldGroup and Field. I suspect you have something messed up in your list handling.


Fry Crayola(Posted 2005) [#5]
Ok, the lists are set up as follows:

Global DBTableList:TList
Global DBFieldGroupList:TList
Global DBFieldList:TList


and then initialised upon the running of the code with

'Initiate System
Function InitialiseDBOne()

	DBTableList = New TList
	DBFieldGroupList = New TList
	DBFieldList = New TList
	
End Function


Each type includes methods, including one to create an object and add it to the relevant list

Table:

Type DBTable
	
	Field Name:String		'name of the table
	Field Id:Int			'Id code for the table
	Field Records:Int		'number of records
	Field MaxRecords:Int	'maximum number of records
	Field MaxSize:Int		'maximum size of area (for dynamic tables)
	Field Mode:Int			'dynamic or static mode
	Field RecordSize:Int	'size of a record (static only)
	Field DataBank:TBank	'bank used to store all data
	Field RecordFlags:TBank	'bank used to store flags for record existence
	Field Offsets:Int[]		'array used to store offsets of records (dynamic). May become bank
	
	'Function to create a new table
	Function CreateTable:DBTable(Name:String, Id:Int, MaxRecords:Int, Mode:Int, MaxSize:Int)
	
		Local this:DBTable = New DBTable
		
		this.Name = Name
		this.Id = Id
		this.Records = 0
		this.MaxRecords = MaxRecords
		this.Mode = Mode
		this.RecordSize = -1
		this.Offsets = New Int[MaxRecords]
		
		TotalTables = TotalTables + 1
		
		ListAddLast(DBTableList, this)
		Return this
				
	End Function
		
	
End Type


FieldGroup
Type DBFieldGroup

	Field Name:String		'name of the field group
	Field Id:Int			'Id code for the group
	Field Size:Int			'size of field group in bytes (depends on component fields)
	Field Table:Int			'ID of parent table
	Field Offset:Int		'offset of field group from start of record

	Function CreateFieldGroup:DBFieldGroup(Name:String, Id:Int, Table:Int)
	
		Local this:DBFieldGroup = New DBFieldGroup
		
		this.Name = Name
		this.Id = Id
		this.Size = -1
		this.Table = Table
		this.Offset = -1
		
		TotalFieldGroups = TotalFieldGroups + 1
		
		ListAddLast(DBFieldGroupList, this)
		Return this
		
	End Function
	
End Type


Field


Type DBField
	
	Field Name:String		'name of the field
	Field Id:Int			'ID code for the field
	Field DataType:Int		'type of data (String, int, BitPattern etc.)
	Field Size:Int			'size of data (e.g. characters, max value)
	Field BitSize:Int		'size of data in bits
	Field Offset:Int		'offset in bits of field from group
	Field Group:Int			'ID of parent group
	Field Table:Int			'ID of parent table
	
	'Values
	Field StrValue:String	'string value (if string)
	Field IntValue:Int		'int value (if int)
	Field DefaultStr:String	'default string value
	Field DefaultInt:Int	'default int value
	Field Record:Int		'current record stored
	
	Function CreateField:DBField(Name:String, Id:Int, DataType:Int, Size:Int, Group:Int, Table:Int, Str:String = "", Integer:Int = 0)
	
		Local this:DBField = New DBField
		
		this.Name = Name
		this.Id = Id
		this.DataType = DataType
		this.Size = Size
		this.Offset = -1
		this.Group = Group
		this.Table = Table
		this.DefaultInt = Integer
		this.DefaultStr = Str
		
		If this.DataType = 0 Then
			this.BitSize = this.Size * 8
		Else If this.DataType = 2 Then
			this.BitSize = this.Size
		Else
			this.BitSize = GetBitSize(this.Size)
		End If
		
		TotalFields = TotalFields + 1
		
		ListAddLast(DBFieldList, this)
		Return this
	
	End Function
	
	Method ReadValue(rec:Int)
		Record = rec
		If DataType = 0 Then ThisReadStr() Else ThisReadInteger()
	End Method
	
	Method WriteValue(rec:Int)
		Record = rec
		If DataType = 0 Then ThisWriteStr() Else ThisWriteInteger()
	End Method
	
	Method ThisReadStr()
	
		'get record offset
		Local RecordOff
		
		'Get table for databank
		For Local tbl:DBTable = EachIn DBTableList
			If tbl.id = Table Then
				RecordOff = tbl.RecordSize * record
				StrValue = ReadStr(tbl.DataBank, RecordOff + (Offset / 8), Size)
			End If
		Next
	
	End Method
	
	Method ThisWriteStr()
	
		'get record offset
		Local RecordOff		
	
		'get table for databank
		For Local tbl:DBTable = EachIn DBTableList
			If tbl.id = Table Then
				RecordOff = tbl.RecordSize * record
				WriteStr(tbl.DataBank, RecordOff + (Offset / 8), StrValue)
			End If
		Next
	
	End Method
	
	Method ThisReadInteger()
	
		'get record offset
		Local RecordOff
	
		'get table for databank
		For Local tbl:DBTable = EachIn DBTableList
			If tbl.id = Table Then
				RecordOff = tbl.RecordSize * record
				IntValue = ReadInteger(tbl.DataBank, (RecordOff * 8) + Offset, BitSize)
			End If
		Next
	
	End Method
	
	Method ThisWriteInteger()
	
		'get record offset
		Local RecordOff
	
		'get table for databank
		For Local tbl:DBTable = EachIn DBTableList
			If tbl.id = Table Then
				RecordOff = tbl.RecordSize * record
				WriteInteger(tbl.DataBank, (RecordOff * 8) + Offset, BitSize, IntValue)
			End If
		Next
	
	End Method
	
	Method StoreString(str:String)
		
		If str.length <= Size Then
			StrValue = str
		Else
			StrValue = str[0..(size-1)]
		End If
		
	End Method
	
	Method StoreInt(integer:Int)
		
		If integer > Size Then
			integer = 0
		End If
		
		IntValue = integer
		
	End Method
	
End Type


Throughout the code, the objects are only accessed via those methods, and directly during the Save and Load functions.


Scott Shaver(Posted 2005) [#6]
I don't suppose you can send me a zip file with the program and source I would like to see it running in front of me. Check my profile for an email addr.


Fry Crayola(Posted 2005) [#7]
I've sent the two source files, haven't got access to the built files right now as I'm posting from work (no net access at home at the moment, sadly).

Thanks


Scott Shaver(Posted 2005) [#8]
Okay I got the files, I'm having a hard time making it screw up.

Test 1:
Started a new database
Added a new table "test"
added a Group "group1"
added field "string" type String Max chars 10 Default 0123456789
added field "int" type Int Max value 255 Default 0
Saved db1
reloaded no problem

Test 2:
Load db1
Add 3 records to table "test"
abcdefghij,1
bcdefghijk,2
cdefghijkl,3
view records, okay
save db2
quit
run
load db2
view records, okay

Test 3:
Added a new table "second"
added a Group "group2"
added field "string" type String Max chars 10 Default 0123456789
added field "int" type Int Max value 255 Default 0
Saved db3
quit
run
load db3
view records, okay

Test 4:
Add 1 records to table "second"
abcdefghij,1
view records, okay
save db4
quit
run
load db4
view records, okay

Test 5:
Added a new table "third"
added a Group "group3"
added field "string" type String Max chars 10 Default 0123456789
added field "int" type Int Max value 255 Default 0
Saved db4
quit
run
load db4
view records, okay

EDIT: just notice that if you enter a string in a string field that has a max chars of 10 and the string is longer than 10 it truncates the string to 9 characters. Still saves and load okay though.


Scott Shaver(Posted 2005) [#9]
I haven't been able to lock it down yet but there is something wrong with tables that have more than one int field.


EDIT:

okay the problem shows when you have two or more tables where one or more of them have multiple int fields. You don't even have to save/load the database.

EDIT:

well you've got me it appears to be totally random when it fails, except that I've been able to make it fail with tables that use multiple int fields.


Scott Shaver(Posted 2005) [#10]
Another possible problem, create a single table with a single group with a string and an int field. enter a single record then try to view the record. the program doesn't list the value of the int field only the string field and it seems to hang until you hit enter.

Here is the out of the session where i did this.

DatabaseOne Main Menu
1. Load Database
2. Save Database
3. Create Table
4. Edit Table
5. Data Entry
6. View Records
7. View Tables
8. Exit
9. DEBUG TABLE BANKS
Select option: 3

Table Name: table1
Maximum Records: 10
Mode (Static/Dynamic): static

Field Group Name: group1
Number of Fields: 2

Field Name: string1
Data Type (String/Int/Bit): string
Size (Characters/Max Value/Bits): 10
Default: empty

Field Name: int1
Data Type (String/Int/Bit): int
Size (Characters/Max Value/Bits): 255
Default: 0

Another Field Group (y/n)? n
Now Packing...
Memory Used: 6730

DatabaseOne Main Menu
1. Load Database
2. Save Database
3. Create Table
4. Edit Table
5. Data Entry
6. View Records
7. View Tables
8. Exit
9. DEBUG TABLE BANKS
Select option: 7

0. table1 (STATIC, 0/10)
View any table, -1 to exit: 0

Table: table1

Group: group1
0, string1 (STRING, 10 Chars)
1, int1 (INT, Max 255)

Link Field (-1 to exit): -1
Memory Used: 6730

DatabaseOne Main Menu
1. Load Database
2. Save Database
3. Create Table
4. Edit Table
5. Data Entry
6. View Records
7. View Tables
8. Exit
9. DEBUG TABLE BANKS
Select option: 5
0, table1 (0/10)
Enter data for which table? 0

Group: group1
string1: Scott
int1: 1
Memory Used: 6752

DatabaseOne Main Menu
1. Load Database
2. Save Database
3. Create Table
4. Edit Table
5. Data Entry
6. View Records
7. View Tables
8. Exit
9. DEBUG TABLE BANKS
Select option: 6
0, table1 (1/10)
View data for which table? 0
0, Scott 

Group: group1
string1: Scott 
Memory Used: 6762

DatabaseOne Main Menu
1. Load Database
2. Save Database
3. Create Table
4. Edit Table
5. Data Entry
6. View Records
7. View Tables
8. Exit
9. DEBUG TABLE BANKS
Select option: 7

0. table1 (STATIC, 1/10)
View any table, -1 to exit: 0

Table: table1

Group: group1
0, string1 (STRING, 10 Chars)
1, int1 (INT, Max 255)

Link Field (-1 to exit): 
Process terminated




Fry Crayola(Posted 2005) [#11]
Thanks for your help so far Scott, I'll get a good look at those issues tonight.

I think I may have solved any direct saving/loading issue. The way I was writing strings to the databank, I neglected to fill the remainder of the allocated space (e.g. writing a four-character string to a ten-character space, I'd need to fill the last six with spaces) which then caused problems.

This space would have been a list of null characters (byte value #00). This is fine, until you read the value from the bank - instead of getting, say, "TEST", you'd get "TEST" followed by 16 null values, which completely screws up the saving and subsequent loading. This explains why it only occurs in the Field type - as it is only fields that read and write data to Banks. By filling the remainder with space characters, and using Trim() after reading them, the problem seems to have disappeared.

I'm getting a few other problems which may be related to what you've pointed out, so I'll give those a whirl. It's also good to note that sometimes the Output screen of BlitzMax fails to display properly - it hangs as you have indicated. It should work fine in a normal command window when running the EXE directly. If not, it's definitely a bug (probably a bug anyway, but I'm baffled by it).