CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/cartodb/models/table.js

Summary

Maintainability
F
2 wks
Test Coverage
/**
 * models for cartodb admin
 */

(function() {

  cdb.admin.SQL = function() {
      var username = (this.options && this.options.user_data)? this.options.user_data.username :
        (window.user_data? window.user_data.username : window.user_name);
      var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
        (window.user_data? window.user_data.api_key : window.api_key);


      return new cartodb.SQL({
        user: username,
        api_key: api_key,
        sql_api_template: cdb.config.getSqlApiBaseUrl()
      });
  }

  cdb.admin.Column = cdb.core.Model.extend({

    idAttribute: 'name',

    url: function(method) {
      var version = cdb.config.urlVersion('column', method);
      var table = this.table || this.collection.table;
      if(!table) {
        cdb.log.error("column has no table assigned");
      }

      var base = '/api/' + version + '/tables/' + table.get('name') + '/columns/';
      if (this.isNew()) {
        return base;
      }
      return base + this.id;
    },


    initialize: function() {
      this.table = this.get('table');
      if(!this.table) {
        throw "you should specify a table model";
      }
      this.unset('table', { silent: true });
    },

    toJSON: function() {
      var c = _.clone(this.attributes);
      // this hack is created to create new column
      // if you set _name instead name backbone does not get
      // it as idAttribute so launch a POST instead of a PUT
      if(c._name) {
        c.name = c._name;
        delete c._name;
      }
      return c;
    },

  });


  /**
   * contains information about the table, not the data itself
   */
  cdb.admin.CartoDBTableMetadata = cdb.ui.common.TableProperties.extend({

    currentLoading: 0, // class variable (shared). I'm still not sure if this is messy as hell or powerfull as a transformer
    sqlApiClass: cartodb.SQL,

    _TEXTS: {
      columnDeleted: 'Your column has been deleted',
      columnDeleting: 'Deleting your column',
      columnAdded: 'Your column has been added',
      columnAdding: 'Adding new column'
    },

    hiddenColumns: [
      'the_geom',
      'the_geom_webmercator',
      'cartodb_georef_status',
      'created_at',
      'updated_at',
      'cartodb_id'
    ],

    initialize: function() {
      _.bindAll(this, 'notice');
      this.readOnly = false;
      this.bind('change:schema', this._prepareSchema, this);
      this._prepareSchema();
      this.sqlView = null;
      this.synchronization = new cdb.admin.TableSynchronization();
      this.synchronization.linkToTable(this);
      this.synchronization.bind('change:id', function isSyncChanged() {
        this.trigger('change:isSync', this, this.synchronization.isSync());
      }, this);
      if (this.get('no_data_fetch')) {
        this.no_data_fetch = true;
        delete this.attributes.no_data_fetch;
      }
      this.data();
      this.bind('error', function(e, resp) {
        this.error('', resp);
      }, this);
      this._data.bind('error', function(e, resp) {
        this.notice('error loading rows, check your SQL query', 'error', 5000);
      }, this);

      this._data.bind('reset', function() {
        var view = this._data;
        this.set({
          schema: view.schemaFromData(this.get('schema')),
          geometry_types: view.getGeometryTypes()
        });
      }, this);

      this.retrigger('change', this._data, 'data:changed');
      this.retrigger('saved', this._data, 'data:saved');

      this.bind('change:table_visualization', function() {
        this.permission = new cdb.admin.Permission(this.get('table_visualization').permission);
        this.trigger('change:permission', this, this.permission);
      }, this);

      // create permission if permission is set
      this.permission = new cdb.admin.Permission(this.get('permission'));
    },

    url: function(method) {
      var version = cdb.config.urlVersion('table', method);
      var base = '/api/' + version + '/tables';
      if (this.isNew()) {
        return base;
      }
      return base + '/' + this.id;
    },

    // use the name as the id since the api works
    // in the same way to table name and id
    parse: function(resp, xhr) {
      if(resp.name) {
        resp.id = resp.name;
      }
      // move geometry_types to stats one
      // geometry_types from backend are not reliable anymore and it can only be used
      // for non editing stuff (showing icons, general checks on table list)
      resp.stats_geometry_types = resp.geometry_types;
      delete resp.geometry_types;
      delete resp.schema;
      return resp;
    },

    notice: function(msg, type, timeout) {
      this.trigger('notice', msg, type, timeout);
    },

    setReadOnly: function(_) {
      var trigger = false;
      if (this.readOnly !== _) {
        trigger = true;
      }
      this.readOnly = _;
      if (trigger) {
        this.trigger('change:readOnly', this, _);
      }
    },

    isReadOnly: function() {
      return this.readOnly || this.data().isReadOnly() || this.synchronization.isSync();
    },

    isSync: function() {
      return this.synchronization.isSync();
    },

    getUnqualifiedName: function() {
      var name = this.get('name');
      if (!name) return null;
      var tk = name.split('.');
      if (tk.length == 2) {
        return tk[1];
      }
      return name;
    },

    // "user".table -> user.table
    getUnquotedName: function() {
      var name = this.get('name');
      return name && name.replace(/"/g, '');
    },

    sortSchema: function() {
      this.set('schema', cdb.admin.CartoDBTableMetadata.sortSchema(this.get('schema')));
    },

    error: function(msg, resp) {
      var err =  resp && resp.responseText && JSON.parse(resp.responseText).errors[0];
      this.trigger('notice', msg + ": " + err, 'error');
    },

    _prepareSchema: function() {
      var self = this;
      this._columnType = {};

      _(this.get('schema')).each(function(s) {
        self._columnType[s[0]] = s[1];
      });

      if (!this.isInSQLView()) {
        self.set('original_schema', self.get('schema'));
      }
    },

    columnNames: function(sc) {
      sc = sc || 'schema'
      return _(this.get(sc)).pluck(0);
    },

    containsColumn: function(name) {
      return _.contains(this.columnNames(), name);
    },

    columnNamesByType: function(type, sc) {
      sc = sc || 'schema';
      var t = _(this.get(sc)).filter(function(c) {
        return c[1] == type;
      });
      return _(t).pluck(0);
    },

    // return geometry columns calculated backend stats
    // use geomColumnTypes if you need something reliable (but slower and async)
    statsGeomColumnTypes: function(geometryTypes) {
      return this.geomColumnTypes(this.get('stats_geometry_types'))
    },

    // return the current column types in an array
    // the values inside the array can be:
    //  'point', 'line', 'polygon'
    geomColumnTypes: function(geometryTypes) {
      var types = geometryTypes || this.get('geometry_types');
      var geomTypes = [];
      if (!_.isArray(types)) {
        return [];
      }
      var _map = {
        'st_multipolygon': 'polygon',
        'st_polygon': 'polygon',
        'st_multilinestring': 'line',
        'st_linestring': 'line',
        'st_multipoint': 'point',
        'st_point': 'point'
      };
      for(var t in types) {
        var type = types[t];
        // when there are rows with no geo type null is returned as geotype
        if(type) {
          var a = _map[type.toLowerCase()]
          if(a) {
            geomTypes.push(a)
          }
        }
      }
      return _.uniq(geomTypes);
    },

    /**
     *  Adding a new geometry type to the table
     *  @param geom type {st_polygon, st_point,...}
     *  @param set options
     */
    addGeomColumnType: function(t, opts) {
      if(!t) return;
      var types = _.clone(this.get('geometry_types')) || [];
      if(!_.contains(types, t)) {
        types.push(t);

        this.set({
          'geometry_types': types
        }, opts);
      }
    },

    nonReservedColumnNames: function() {

      var self = this;
      return _.filter(this.columnNames(), function(c) {
        return !self.isReservedColumn(c);
      });
    },

    columnTypes: function() {
      return _.clone(this._columnType);
    },

    _getColumn: function(columnName) {
      if(this._columnType[columnName] === undefined) {
        return
        // throw "the column does not exists";
      }
      var c = new cdb.admin.Column({
        table: this,
        name: columnName,
        type: this._columnType[columnName]
      });
      return c;
    },

    getColumnType: function(columnName, sc) {
      sc = sc || 'schema';
      var t = _(this.get(sc)).filter(function(c) {
        return c[0] == columnName;
      });
      if(t.length > 0)
        return t[0][1];
      return;
    },

    addColumn: function(columnName, columnType, opts) {
      var self = this;
      var c = new cdb.admin.Column({
        table: this,
        _name: columnName,
        type: columnType || 'string'
      });
      this.notice(self._TEXTS.columnAdding, 'load', 0);
      c.save(null, {
          success: function(mdl, obj) {
            self.notice(self._TEXTS.columnAdded, 'info');
            self.trigger('columnAdd', columnName);
            self.data().fetch();
            opts && opts.success && opts.success(mdl,obj);
          },
          error: function(e, resp) {
            self.error('error adding column', resp);
            opts && opts.error && opts.error(e);
          },
          wait: true
      });
    },

    deleteColumn: function(columnName, opts) {
      var self = this;
      var c = this._getColumn(columnName);
      if (c !== undefined) {
        this.notice(self._TEXTS.columnDeleting, 'load', 0);
        c.destroy({
            success: function() {
              self.trigger('columnDelete', columnName);
              self.notice(self._TEXTS.columnDeleted, 'info');
              self.data().fetch();
              opts && opts.success && opts.success();
            },
            error: function(e, resp) {
              self.error('error deleting column', resp);
              opts && opts.error && opts.error();
            },
            wait: true
        });
      }
    },

    renameColumn: function(columnName, newName, opts) {
      if(columnName == newName) return;
      var self = this;
      var c = this._getColumn(columnName);
      var oldName = c.get('name');
      c.set({
        new_name: newName,
        old_name: c.get('name')
      });
      this.notice('renaming column', 'load', 0);
      c.save(null,  {
          success: function(mdl, data) {
            self.notice('Column has been renamed', 'info');
            self.trigger('columnRename', newName, oldName);
            self.data().fetch();
            opts && opts.success && opts.success(mdl, data);
          },
          error: function(e, resp) {
            cdb.log.error("can't rename column");
            self.error('error renaming column', resp);
            opts && opts.error && opts.error(e, resp);
          },
          wait: true
      });
    },

    isTypeChangeAllowed: function(columnName, newType) {
      var deactivateMatrix = {
        'number': ['date'],
        'boolean': ['date'],
        'date': ['boolean']
      };
      var c = this._getColumn(columnName);
      if (!c) {
        return true;
      }
      var type = c.get('type');
      var deactivated = deactivateMatrix[type] || [];
      deactivated = deactivated.concat([type])
      return !_.contains(deactivated, newType);
    },

    isTypeChangeDestructive: function(columnName, newType) {
      var columnType = this.getColumnType(columnName);

      var destructiveMatrix = {
        "string": {
          "string": false,
          "number": true,
          "date": true,
          "boolean": true,
        },
        "number": {
          "string": false,
          "number": false,
          "date": true,
          "boolean": true,
        },
        "date": {
          "string": false,
          "number": true,
          "date": false,
          "boolean": true,
        },
        "boolean": {
          "string": false,
          "number": false,
          "date": true,
          "boolean": false,
        },
      }
      return destructiveMatrix[columnType][newType]
    },

    changeColumnType: function(columnName, newType, opts) {
      var self = this;
      var c = this._getColumn(columnName);

      if(this.getColumnType(columnName) == newType) {
        opts && opts.success && opts.success();
        return;
      }
      this.saveNewColumnType(c, newType, opts);
    },

    saveNewColumnType: function(column, newType, opts) {
      var self = this;
      column.set({ type: newType});
      this.notice('Changing column type', 'load', 0);
      column.save(null, {
          success: function() {
            self.notice('Column type has been changed', 'info');
            self.trigger('typeChanged', newType); // to make it testable
            self.data().fetch();
            opts && opts.success && opts.success();
          },
          error: function(e, resp) {
            self.trigger('typeChangeFailed', newType, e); // to make it testable
            self.error('error changing column type', resp);
            opts && opts.error && opts.error(e, resp);
          },
          wait: true
      });
    },

    /**
     * returns the original data for the table not the current applied view
     */
    originalData: function() {
      return this._data;
    },

    data: function() {
      var self = this;
      if(this._data === undefined) {
        this._data = new cdb.admin.CartoDBTableData(null, {
          table: this
        });
        this.bindData();
      }
      if(this.sqlView) {
        return this.sqlView;
      }
      return this._data;
    },

    bindData: function(data) {
      var self = this;
      if(this._data && !this._data.bindedReset) {

        this.retrigger('reset', this._data, 'dataLoaded');
        this.retrigger('add', this._data, 'dataAdded');
        this._data.bindedReset = true;

      }
      if(this.sqlView && !this.sqlView.bindedReset) {
        this.retrigger('reset', this.sqlView, 'dataLoaded');
        this.retrigger('add', this.sqlView, 'dataAdded');
        this.sqlView.bindedReset = true;
      }

    },

    useSQLView: function(view, options) {
      if (!view && !this.sqlView) return;
      options = options || {};
      var self = this;
      var data = this.data();

      if(this.sqlView) {
        this.sqlView.unbind(null, null, this);
        this.sqlView.unbind(null, null, this._data);
      }

      // reset previous
      if(!view && this.sqlView) {
        this.sqlView.table = null;
      }

      this.sqlView = view;
      this.bindData();

      if(view) {
        view.bind('reset', function() {
          if(!view.modify_rows) {
            this.set({
              schema: view.schemaFromData(this.get('schema')),
              geometry_types: view.getGeometryTypes()
            });
          }
        }, this);
        // listen for errors
        view.bind('error', function(e, resp) {
          this.notice('error loading rows, check your SQL query', 'error', 5000);
        }, this);

        view.bind('loading', function() {
          //this.notice(_t('loading query'), 'load', 0);
        }, this);

        view.bind('reset loaded', function() {
          if(view.modify_rows) {
            this.notice(view.affected_rows + ' rows affected');
            this.useSQLView(null);
          } else {
            this.notice(_t('loaded'));
          }
        }, this);

        // swicth source data
        this.dataModel = this.sqlView;
        view.table = this;
      } else {
        this.dataModel = this._data;
        // get the original schema
        self.set({
          'schema': self.get('original_schema')
        });///*, { silent: true });
        self.data().fetch();
      }
      this.trigger('change:dataSource', this.dataModel, this);
    },

    isInSQLView: function() {
      return this.sqlView ? true: false;
    },

    /**
     * replace fetch functionally to add some extra call for logging
     * it can be used in the same way fetch is
     */
    fetch: function(opts) {
      var self = this;
      silent = opts? opts.silent : false;
      if(!silent) {
        this.notice('loading table', 'load', this, 0, 0);
      }
      var xhr = this.elder('fetch', opts)
      $.when(xhr).done(function() {
        opts && opts.success && opts.success.old_success && opts.success.old_success();
        if(!silent) {
          self.notice('loaded');
        }
      }).fail(function(){
        if(!silent) {
          self.notice('error loading the table');
        }
      });
      return xhr;

    },

    isReservedColumn: function(c) {
      return cdb.admin.Row.isReservedColumn(c);
    },

    /**
     * when a table is linked to a infowindow each time a column
     * is renamed or removed the table pings to infowindow to remove
     * or rename the fields
     */
    linkToInfowindow: function(infowindow) {
      var self = this;
      this.bind('columnRename', function(newName, oldName) {
        if(infowindow.containsField(oldName)) {
          infowindow.removeField(oldName);
          infowindow.addField(newName);
        }
      }, infowindow);
      this.bind('columnDelete', function(oldName, newName) {
        infowindow.removeField(oldName);
      }, infowindow);

      this.bind('change:schema', function() {
        var self = this;
        var columns = _(this.columnNames()).filter(function(c) {
          return !_.contains(infowindow.SYSTEM_COLUMNS, c);
        });

        function _hash(str){
            var hash = 0, c;
            for (i = 0; i < str.length; i++) {
                c = str.charCodeAt(i);
                hash = c + (hash << 6) + (hash << 16) - hash;
            }
            return hash;
        }

        if (this.isInSQLView()) {
          if (!infowindow.has('defaul_schema_fields')) {
            infowindow.saveFields('defaul_schema_fields');
          }
          var current_schema_key = 'schema_' + _hash(self.columnNames().sort().join(''));
          var previous_schema_key = 'schema_' + _hash(
            _(self.previous('schema')).pluck(0).sort().join('')
          );

          if(!infowindow.has(previous_schema_key)) {
            infowindow.saveFields(previous_schema_key);
          }
          if(infowindow.has(current_schema_key)) {
            infowindow.restoreFields(null, current_schema_key);
          }
        } else {
          infowindow.restoreFields(null, 'defaul_schema_fields');
        }

        if (infowindow.get('template')) {
          // merge fields checking actual schema
          infowindow.mergeFields(columns);
        } else {
          // remove fields that no longer exist
          infowindow.removeMissingFields(columns);
        }
      }, this);

    },

    embedURL: function() {
      return "/tables/" + this.get('name') + "/embed_map"
    },

    /**
     * @deprecated use vis.viewUrl() or vis.viewUrl(currentUser) instead.
     */
    viewUrl: function() {
      return cdb.config.prefixUrl() + "/tables/" + this.getUnqualifiedName()
    },

    hasTheGeom: function() {
      var currentSchema = this.get('schema');
      // if we have "the_geom" in our current schema, returnstrue
      for(var n in currentSchema) {
        if(currentSchema[n][0] === 'the_geom') {
          return true;
        }
      }
      return false;
    },

    /**
     * Checks the server to see if the table has any georeferenced row, independently of the applyed query
     * @return {promise}
     */
    fetchGeoreferenceStatus: function() {
      var dfd = $.Deferred();
      var username = (this.options && this.options.user_data)? this.options.user_data.username :
        (window.user_data? window.user_data.username : window.user_name);
      var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
        (window.user_data? window.user_data.api_key : window.api_key);


      this.sqlApi = new this.sqlApiClass({
        user: username,
        version: 'v1',
        api_key: api_key,
        sql_api_template: cdb.config.getSqlApiBaseUrl()
      });

      var sql = 'SELECT the_geom FROM ' + this.get('name') + ' WHERE the_geom is not null';
      this.sqlApi.execute(sql).done(function(data){
        if(data.rows.length > 0) {
          dfd.resolve(true);
        } else {
          dfd.resolve(false);
        }
      });

      return dfd.promise();

    },

    /**
     * Checks the current loaded records to see if they are georeferenced
     * @return {boolean}
     */
    isGeoreferenced: function() {
      var geoColumns = this.geomColumnTypes();
      if(geoColumns && geoColumns.length > 0) {
        return true;
      } else {
        if (!this.isInSQLView()) {
          // sometimes the columns are changed in the frontend site
          // and the geocolumns are not updated.
          // check the columns in local
          return this._data.any(function(row) {
            return row.hasGeometry();
          });
        }
      }
      return false;
    },

    /**
     * this function can only be called during change event
     * returns true if the geometry type has changed
     * for this method multipolygon and polygon are the same geometry type
     */
    geometryTypeChanged: function() {
      if (!('geometry_types' in this.changed)) return false;
      var geoTypes = this.get('geometry_types')
      var prevGeoTypes = this.previousAttributes().geometry_types;
      function normalize(e) {
        e = e.toLowerCase();
        if (e === 'st_multipolygon') {
          return 'st_polygon'
        }
        if (e === 'st_multilinestring') {
          return 'st_linestring'
        }
        if (e === 'st_multipoint') {
          return 'st_point'
        }
        return e;
      }

      if(!geoTypes ||
        geoTypes.length === 0 ||
        !prevGeoTypes ||
        prevGeoTypes.length === 0) {
        return true;
      }

      var n = normalize(geoTypes[0]);
      var o = normalize(prevGeoTypes[0]);
      return n !== o;
    },

    /**
     * Get necessary data create a duplicated dataset from this table.
     *
     * @param {Object} newName name of new dataset.
     * @param {Object} callbacks
     * @returns {Object}
     */
    duplicate: function(newName, callbacks) {
      callbacks = callbacks || {};

      // Extracted from duplicate_table_dialog
      var data = {
        table_name: newName
      };

      // Set correct data object, depending on if the app has a query applied or not
      if (this.isInSQLView()) {
        var query = this.data().getSQL();
        data.sql = ( !query || query == "" ) ? 'SELECT * FROM ' + cdb.Utils.safeTableNameQuoting(this.get('name')) : query;
      } else {
        data.table_copy = this.get('name');
      }

      var importModel = new cdb.admin.Import();
      importModel.save(data, {
        error: callbacks.error,
        success: function(model, changes) {
          var checkImportModel = new cdb.admin.Import({
            item_queue_id: changes.item_queue_id
          });

          checkImportModel.bind('importComplete', function() {
            checkImportModel.unbind();

            // So import is done, create new table object from the new table and fetch, callback once finished.
            var newTable = new cdb.admin.CartoDBTableMetadata({
              id: checkImportModel.get('table_id')
            });

            newTable.fetch({
              success: function() {
                callbacks.success(newTable);
              },
              error: callbacks.error
            });
          });

          checkImportModel.bind('importError', function() {
            checkImportModel.unbind();
            callbacks.error.apply(this, arguments);
          });

          checkImportModel.pollCheck();
        }
      });
    },

    /**
     * Get the visualizations that are using this table dataset.
     * Note! a .fetch() is required to be sure the data to be available.
     * @return {Array}
     */
    dependentVisualizations: function() {
      // dependent = visualizations with a single layer
      // non-dependant = have more than this dataset as a layer
      return _.chain(this.get('dependent_visualizations'))
        .union(this.get('non_dependent_visualizations'))
        .compact()
        .value() || [];
    }

  }, {
    /**
     * creates a new table from query
     * the called is responsable of calling save to create
     * the table in the server
     */
    createFromQuery: function(name, query) {
      return new cdb.admin.CartoDBTableMetadata({
        sql: query,
        name: name
      });
    },

    sortSchema: function(schema) {
      var priorities = {
        'cartodb_id': 1,
        'the_geom': 2,
        '__default__': 3,
        'created_at': 4,
        'updated_at': 5
      };

      function priority(v) {
        return priorities[v] || priorities['__default__'];
      }

      return _.chain(schema)
        .clone()
        .sort(function(a, b) { // ..and then re-sort by priorities defined above
          var prioA = priority(a[0]);
          var prioB = priority(b[0]);
          if (prioA < prioB) {
            return -1;
          } else if (prioA > prioB) {
            return 1;
          } else { //priority is the same (i.e. __default__), so compare alphabetically
            return a[0] < b[0] ? -1 : 1;
          }
        })
        .value();
    },

    /**
     * return true if the sql query alters table schema in some way
     */
    alterTable: function(sql) {
      sql = sql.trim();
      return sql.search(/alter\s+[\w\."]+\s+/i) !== -1   ||
             sql.search(/drop\s+[\w\.\"]+/i)  !== -1  ||
             sql.search(/^vacuum\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^create\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^reindex\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^grant\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^revoke\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^cluster\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^comment\s+on\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^explain\s+[\w\.\"]+/i)  !== -1;
    },

    /**
     * return true if the sql query alters table data
     */
    alterTableData: function(sql) {
      return this.alterTable(sql)       ||
             sql.search(/^refresh\s+materialized\s+view\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/^truncate\s+[\w\.\"]+/i)  !== -1 ||
             sql.search(/insert\s+into/i) !== -1  ||
             sql.search(/update\s+[\w\.\-"]+\s+.*set/i) !== -1  ||
             sql.search(/delete\s+from/i) !== -1;
    }

  });

  cdb.admin.Row = cdb.core.Model.extend({

    _CREATED_EVENT: 'created',
    _CREATED_EVENT: 'creating',
    sqlApiClass: cartodb.SQL,

    _GEOMETRY_TYPES: {
      'point': 'st_point',
      'multipoint': 'st_multipoint',
      'linestring': 'st_linestring',
      'multilinestring': 'st_multilinestring',
      'polygon': 'st_polygon',
      'multipolygon': 'st_multipolygon'
    },

    url: function(method) {
      var version = cdb.config.urlVersion('record', method);
      var table = this.table || this.collection.table;
      if(!table) {
        cdb.log.error("row has no table assigned");
      }

      var base = '/api/' + version + '/tables/' + table.get('name') + '/records/';
      if (this.isNew()) {
        return base;
      }
      return base + this.id;
    },

    fetch: function(opts) {
      opts = opts || {}
      var self = this;
      var silent = opts && opts.silent;
      var username = (this.options && this.options.user_data)? this.options.user_data.username :
        (window.user_data? window.user_data.username : window.user_name);
      var api_key = (this.options && this.options.user_data)? this.options.user_data.api_key :
        (window.user_data? window.user_data.api_key : window.api_key);

      var table = this.table || this.collection.table;

      var sqlApi = new this.sqlApiClass({
        user: username,
        version: 'v2',
        api_key: api_key,
        sql_api_template: cdb.config.getSqlApiBaseUrl(),
        extra_params: ['skipfields']
      });
      // this.trigger('loading')
      var sql = null;
      var columns = table.columnNames()
      if (opts.no_geom) {
        columns = _.without(columns, 'the_geom', 'the_geom_webmercator');
      } else {
        columns = _.without(columns, 'the_geom');
      }
      sql = 'SELECT ' + columns.join(',') + " "
      if(table.containsColumn('the_geom') && !opts.no_geom) {
        sql += ',ST_AsGeoJSON(the_geom, 8) as the_geom '
      }
      sql += ' from (' + table.data().getSQL() + ') _table_sql WHERE cartodb_id = ' + this.get('cartodb_id');
      // Added opts to sql execute function to apply
      // parameters ( like cache ) to the ajax request
      if (opts.no_geom) {
        opts.skipfields = 'the_geom,the_geom_webmercator';
      } else {
        opts.skipfields = 'the_geom_webmercator';
      }
      sqlApi.execute(sql, {}, opts).done(function(data){
        if(self.parse(data.rows[0])) {
          self.set(data.rows[0]);//, {silent: silent});
          self.trigger('sync');
        }
      });

    },

    toJSON: function() {
      var attr = _.clone(this.attributes);
      // remove read-only attributes
      delete attr['updated_at'];
      delete attr['created_at'];
      delete attr['the_geom_webmercator'];
      if(!this.isGeometryGeoJSON()) {
        delete attr['the_geom'];
      }
      return attr;
    },

    isGeomLoaded: function() {
      var geojson = this.get('the_geom');
      var column_types_WKT = cdb.admin.WKT.types
      return  (geojson !== 'GeoJSON' && geojson !== -1 && !_.contains(column_types_WKT, geojson));
    },

    hasGeometry: function() {
      var the_geom = this.get('the_geom');
      return !!(the_geom != null && the_geom != undefined && the_geom != '')
      // in fact, if the_geom has anything but null or '', the row is georeferenced

      // if(typeof the_geom === 'string') {
      //   // if the geom contains GeoJSON, the row has a valid geometry, but is not loaded yet
      //   if(the_geom === 'GeoJSON')  {
      //     return true
      //   }

      //   try {
      //     var g = JSON.parse(the_geom);
      //     return !!g.coordinates;
      //   } catch(e) {
      //     return false;
      //   }
      // } else {
      //   if(the_geom) {
      //     return !!the_geom.coordinates;
      //   }
      //   return false;
      // }
    },
    /**
     * Checks if the_geom contains a valid geoJson
     */
    isGeometryGeoJSON: function() {
      var the_geom = this.get('the_geom');
      if(the_geom && typeof the_geom === 'object') {
        return !!the_geom.coordinates;
      } else if(typeof the_geom !== 'string') {
        return false;
      }
      // if the geom contains GeoJSON, the row has a valid geometry, but is not loaded yet
      if(the_geom === 'GeoJSON')  {
        return true
      }

      try {
        var g = JSON.parse(the_geom);
        return !!g.coordinates;
      } catch(e) {
        return false;
      }

      return false;

    },

    getFeatureType: function() {
      if (this.isGeomLoaded()) {
        // Problem geometry type from a WKB format
        // Not possible for the moment
        try {
          var geojson = JSON.parse(this.get('the_geom'));
          return geojson.type.toLowerCase();
        } catch(e) {
          cdb.log.info("Not possible to parse geometry type");
        }
      }
      return null;
    },

    getGeomType: function() {
      try {
        return this._GEOMETRY_TYPES[this.getFeatureType()];
      } catch(e) {
        cdb.log.info("Not possible to parse geometry type");
      }
    }

  }, {
    RESERVED_COLUMNS: 'the_geom the_geom_webmercator cartodb_id created_at updated_at'.split(' '),
    isReservedColumn: function(c) {
      return _(cdb.admin.Row.RESERVED_COLUMNS).indexOf(c) !== -1;
    }
  });

})();