public static bool ColsAreCompatible(Column colToUpdate, Column colSource) { bool bCompatible = false; if (colToUpdate.colType.Equals(COLUMN_TYPES.AUTOINCREMENT)) { bCompatible = false; // hard stop. Can't update autoincrement fields. } else if (colToUpdate.colType.Equals(colSource.colType)) { bCompatible = true; } else { // TODO: Are there other sets we want to consider equivalent? Queue<COLUMN_TYPES[]> qLinkedTypes = new Queue<COLUMN_TYPES[]>(); COLUMN_TYPES[] sharedIntTypes = { COLUMN_TYPES.INT, COLUMN_TYPES.AUTOINCREMENT, COLUMN_TYPES.TINYINT }; qLinkedTypes.Enqueue(sharedIntTypes); foreach (COLUMN_TYPES[] relatedColTypes in qLinkedTypes) { if (sharedIntTypes.Contains(colToUpdate.colType) && sharedIntTypes.Contains(colSource.colType)) { bCompatible = true; break; } } } return bCompatible && colToUpdate.intColLength >= colSource.intColLength; }
public CompositeColumnValueModifier(string strValue, Column column, bool isAdditionModifierNotSubtraction) { if (string.IsNullOrWhiteSpace(strValue)) { if (null == column) { throw new Exception("Update modifiers must have a value or column to initialize."); } this.column = column; } else { this.strValue = strValue; } this.isAdditionModifierNotSubtraction = isAdditionModifierNotSubtraction; }
public static BaseSerializer routeMe(Column colToRoute) { BaseSerializer serializer = null; switch (colToRoute.colType) { case COLUMN_TYPES.INT: case COLUMN_TYPES.TINYINT: case COLUMN_TYPES.AUTOINCREMENT: serializer = new IntSerializer(colToRoute); break; case COLUMN_TYPES.CHAR: serializer = new CharSerializer(colToRoute); break; // For now, I'm going to cheese out and treat these both // as decimal. As Skeet says [1], they're both *floating point* // representations, even if we don't normally think of FLOATS // the same as we do decimals. // [1] http://tinyurl.com/mzu6t4j // TODO: Create a real float type for huge numbers. case COLUMN_TYPES.FLOAT: case COLUMN_TYPES.DECIMAL: serializer = new DecimalSerializer(colToRoute); break; // TODO: Create a real BIT column type with serializer. case COLUMN_TYPES.BIT: serializer = new IntSerializer(colToRoute); break; case COLUMN_TYPES.DATETIME: serializer = new DateSerializer(colToRoute); break; default: throw new Exception("Illegal/Unimplemented column type: " + colToRoute.colType); } return serializer; }
// inherited Column colRelated // Apparently I need to "manually chain" constructors? // http://stackoverflow.com/a/7230589/1028230 public DateSerializer(Column colToUseAsBase) : base(colToUseAsBase) { this.colRelated = colToUseAsBase; }
public BaseSerializer(Column colToUseAsBase) { this.colRelated = colRelated; }
private void _prepareTableFromFile() { if (_strTableName.Equals("") || _strTableParentFolder.Equals("")) { throw new Exception("Attempted to prepare a table from a manager that has not been initialized."); } else { FileInfo info = new FileInfo(this.strTableFileLoc); // this is really the "come on, stop looking already" breakpoint. // TODO: Consider something less than 9,223,372,036,854,775,807 / 2. // Though, admittedly, that's a random thing to try and describe as // an absolute ceiling. (Divided by 2 b/c there are always at least // two lines of metadata at the start of a mdbf file.) long lngStopTryingAt = (info.Length / 2) + 1; // TODO: Laziness alert with the +1. if (MainClass.bDebug) Console.WriteLine("Preparing table from: " + info.FullName); int intPullSize = 500; int intLineLength = -1; // we can't know until we've actually found the first 0x11 0x11 combo. int intStartingByte = 0; // I'm currently just starting at 0, reading ~500 bytes from a stream, looking for 0x11, 0x11, then, // if I haven't found it yet, scrolling forward to read approximately the next 500, ad infinitum // This is, admittedly, *NOT* the best way to accomplish reading far enough into the stream to find // the end of the metadata lines. bool bFoundLineEnd = false; while (!bFoundLineEnd) { FileStream fs = File.Open(this.strTableFileLoc, FileMode.Open); byte[] abytFirstLine = Utils.ReadToLength(fs, intPullSize + intStartingByte); fs.Close(); // TODO: Keep open until you're done; closing now to avoid leaving it open when I break/stop when debugging // Now see if we've gotten to the first 0x11, 0x11 line end. // Note: The real disadvantage of this routine is if a field length // is 4369 (decimal), which is 0x1111. Ooops! So 4369 is out as a length. // (as is 69904-69919, etc etc, but let's pretend nobody's doing something // that long for now. I mean, heck, 4369 is ludicrous enough). for (int i = intStartingByte; i < abytFirstLine.Length - 1; i++) { if (0x11 == abytFirstLine[i] && 0x11 == abytFirstLine[i + 1]) { bFoundLineEnd = true; intLineLength = i + 2; // add both 0x11s (remembering array is 0-based) break; } } // see if we found the end of the first line. if (!bFoundLineEnd) { if (abytFirstLine.Length >= lngStopTryingAt) { throw new ImproperDatabaseFileException("Improperly formatted file, or file is not a database file: " + info.FullName); } intStartingByte += (intPullSize - 2); // so we're just re-evaling /some/ of the bytes; no big loss. if (0 == abytFirstLine.Length) { // we're out of bytes. break; } } else { // Start reading out column types and lengths //===================================== // Read out the column names. //===================================== // NOTES: // 1.) Remember the Naming Kludge. The length of the field has been linked to the length // of the column name just to keep spacing in the table simpler (and stopping us from having // table metadata tables. So there are essentially two types of names. // a.) Names that are shorter than the length of the fields. // eg, "spam CHAR(20)" // 20 > "spam".length, so we have the entire name in the name row. // 0x73 0x70 0x61 0x6d 0x00 0x00 0x00... 0x11 // s p a m zero zero zero... end // // b.) Names that are as long or longer than their column length. // eg, "firstName CHAR(6)" // 6 < "firstName".length // 0x66 0x69 0x72 0x73 0x74 0x4e 0x11 // f i r s t N end // Remember that each column ends with a 0x11 marker. Excepting the first two metadata and // name rows, rows can contain 0x11 as well. // // I'm horribly tempted to not use ASCII and to derive a more efficient encoding scheme to store names, // since we're only using [A-Za-z0-9] and $, #, _ -- 65 chars // http://stackoverflow.com/questions/954884/what-special-characters-are-allowed-in-t-sql-column-name // Unfortunately eight bits isn't enough to capture two of those. 65 still needs... // 0 1 0 0 0 0 0 1 // It'd have to fit in one nibble (four bits (16 options)) to do anything cool *and simple*. // // 2.) There are a couple of ways of reading these lines that would be more complicated but also more efficient. // One would be to see if our abytFirstLine is big enough for two lines of info and use two ArraySegments. // Or, if it's not long enough, I could make one read from 0 to 2*intLineLength, cut back into the same code, // and segement that. // // I'm going to painfully and stodgily read in the second line from the original stream. FileStream fs2 = File.Open(this.strTableFileLoc, FileMode.Open); byte[] abytSecondLine = Utils.ReadToLength(fs2, intLineLength, intLineLength); // the reader is zero-based, so start at the exact length position. fs2.Close(); // TODO: Same thing as above -- keep open or, better yet, read from previously opened fs. int j = 1; // use 1 for j initially so we skip the line's initial 0x11 Queue<Column> qColumns = new Queue<Column>(); while (j < intLineLength) { Column column = new Column(); column.strColName = ""; // I think the for loop with breaks is more readable, so I'm going to avoid while (which is what I used initially) // though it might make more sense to while loop until 0x00 or 0x11, and if it doesn't work correctly, the thrown exception // for overshooting the length makes some sense, as the row was misconstructed. int k; // scoped to check it later. for (k = j; k < intLineLength; k++) // use 1 for k so we skip the line's initial 0x11 { if (0x00 == abytSecondLine[k] || 0x11 == abytSecondLine[k]) break; // end of the column information else column.strColName += (char)abytSecondLine[k]; } column.isFuzzyName = 0x00 != abytSecondLine[k]; // if we were forced to end early (0x11 "end of col", not 0x00 "end of name"), we've got a fuzzy name. See above. column.intColStart = j; // start is at the first byte of the entry; this col starts on the jth bit of each row. column.colType = (COLUMN_TYPES)abytFirstLine[j++]; if (Column.IsSingleByteType(column.colType)) { // handle one byte col types column.intColLength = 1; } else { Stack<byte> stackLength = new Stack<byte>(); while (abytFirstLine[j] != 0x00 && abytSecondLine[j] != 0x11) { stackLength.Push(abytFirstLine[j++]); } if (COLUMN_TYPES.AUTOINCREMENT == column.colType) { // We're using the intColLength to store the last autoincrement // value in this column type. column.intColLength = 4; // NOTE: Changing from INT(4) will bork things. column.intAutoIncrementCount = Utils.ByteArrayToInt(stackLength.ToArray()); } else { column.intColLength = Utils.ByteArrayToInt(stackLength.ToArray()); } } // Roll to the end of the column's space. while (abytSecondLine[j] != 0x11) { j++; } column.intColIndex = qColumns.Count; // 0-based index of this column, if they're taken in order. qColumns.Enqueue(column); if (0x11 == abytFirstLine[j + 1]) { break; // if we found two 0x11s in a row, stop. End of the row. } else { j++; // skip the col end 0x11 and get ready for the next column } } // loop back to get next -- while (j < intLineLength) _columns = qColumns.ToArray(); // +1 once for the check, above for the second 0x11 at abytFirstLine[j+1], // then another because we want length, not 0-based byte number in the array. // Note that this should include all the 0x11 overhead. _intRowLength = j + 1 + 1; } // eo bFoundLineEnd == true logic. } // eo while (!bFoundLineEnd) } // eo else for if (_strTableName.Equals("") || _strTableParentFolder.Equals("")) // "initialization check" }
// TODO: For now, we're stogily assuming `val[whitespace][plus or minus][whitespace][value]` etc. // TODO: Even though we're allowing multiple column names and values, this is still pretty naive, // as we're not handling parentheses or order of operations at all. public static byte[] ConstructValue(Column colOutput, string strClause, byte[] abytRowOfValues, TableContext table) { string strOrigClause = strClause; strClause = "+ " + strClause; string[] astrTokens = strClause.Split(); if (0 != astrTokens.Length % 2) { throw new Exception("Illegal update clause (value-operator count mismatch): " + strOrigClause); } Queue<CompositeColumnValueModifier> qModifiers = new Queue<CompositeColumnValueModifier>(); for (int i = 0; i < astrTokens.Length; i = i + 2) { qModifiers.Enqueue( new CompositeColumnValueModifier( astrTokens[i + 1].IsNumeric() ? astrTokens[i + 1] : string.Empty, astrTokens[i + 1].IsNumeric() ? null : table.getColumnByName(astrTokens[i + 1]), !astrTokens[i].Equals("-") ) ); } // I realize I could've done this in the loop where I construct // the UpdateModifiers, but this feels a little cleaner. byte[] abytResult = new byte[colOutput.intColLength]; BaseSerializer outputSerializer = Router.routeMe(colOutput); foreach (CompositeColumnValueModifier modifier in qModifiers) { if (modifier.isValueNotColumn) { abytResult = outputSerializer.addRawToStringRepresentation(abytResult, modifier.strValue, !modifier.isAdditionModifierNotSubtraction); } else { if (colOutput.intColLength < modifier.column.intColLength || !ColsAreCompatible(colOutput, modifier.column)) { throw new Exception("Value aggregation attempted to aggregate values that were potentially too large or with columns of incompatible types."); } byte[] abytValToAdd = new byte[modifier.column.intColLength]; Array.Copy(abytRowOfValues, modifier.column.intColStart, abytValToAdd, 0, modifier.column.intColLength); abytResult = outputSerializer.addRawToRaw(abytResult, abytValToAdd, !modifier.isAdditionModifierNotSubtraction); } } return abytResult; }